HTTP 里没有任何「这条报文没被动过」的证明。 自己加个 MD5 也没用——校验和是公开算法,中间人改完正文顺手重算一遍就行。 能挡住这一手的只有带密钥的校验。
7.1.1 是被读,7.1.2 是被冒充,这一节是被改。 三种风险对应 TLS 的三项保护:机密性、身份认证、完整性。
一、四种情形,只有两种能发现
Demo 里拿一句「转账金额:100 元」做实验,把 100 改成 999,看接收方能不能察觉。 | 情形 | 接收方能不能发现 | |---|:---:| | 没有任何校验 | 不能 | | 正文后附 MD5 | 能 | | 中间人顺手重算 MD5 | 不能 | | 换成 HMAC-SHA256 | 能 | 第三行是关键:MD5、SHA-1、CRC32 全是公开算法,谁拿到正文都能算出一个「正确」的值。 它们防的是意外损坏(磁盘坏道、传输丢字节),不防故意篡改。
二、分水岭是密钥
HMAC 就是在哈希外面套一层密钥:HMAC(密钥, 正文)。没有密钥就算不出正确的值。
Demo 里中间人拿一个猜的密钥去重算,得到 ceb05da1...,而接收方用真密钥算是 45358e4e...,
对不上,于是发现了篡改。
这解释了一件容易被忽略的事:整条链上必须有「只有双方知道的东西」。
没有它,再强的哈希也只是个公开计算器。TLS 的价值就在于握手阶段能协商出这么一份只有两端知道的密钥。
三、完整性保护依附于加密
TLS 里完整性不是单独一层,它和加密是一起做的: AEAD 这一类算法(比如 AES-GCM)在加密的同时生成认证标签,解密时标签不对就直接报错。 这样设计有个好处:攻击者连「这条密文对应哪条明文」都拼不出来,更没法只改其中一段。 相比之下「先加密再单独算 MAC」的组合历史上出过不少拼接类问题。 所以严格说,HTTPS 提供的三项保护里,完整性是搭在加密和密钥上的。 没有前面的密钥协商,完整性保护就无从谈起。
四、动手 Demo:改一个数字,看四种校验怎么反应
把正文里「100 元」改成「999 元」, 然后分别用「无校验」「带 MD5」「中间人重算 MD5」「带密钥的 HMAC」四种方式交付, 看接收方每一轮能不能发现。
body = u'转账金额:100 元'.encode('utf-8')
attacked = u'转账金额:999 元'.encode('utf-8')
# 情形二:附一个公开算法的校验和
d1 = hashlib.md5(body).hexdigest()
d2 = hashlib.md5(attacked).hexdigest() # 中间人也能算
# 情形四:换成带密钥的
secret = b'only-bozai-knows'
h1 = hmac.new(secret, body, hashlib.sha256).hexdigest()
h2 = hmac.new(secret, attacked, hashlib.sha256).hexdigest()
guess = hmac.new(b'attacker-guess', attacked, hashlib.sha256).hexdigest()
实跑输出:
--------------------------------------------------------------------
校验和挡不住会动手的中间人
--------------------------------------------------------------------
原始正文:转账金额:100 元(22 字节)
篡改后: 转账金额:999 元(22 字节)
四种情形,看接收方能不能发现:
情形一:没有任何校验
$ 收到 转账金额:999 元
结果:用掉了,一分钱没少改。接收方**无从发现**。
情形二:正文后面附一个 MD5
发出时:正文 + md5=41240b03f1aa3449
收到时:正文 + md5=310de3bb1f169613
本地重算 md5=310de3bb1f169613
结果:对不上,**发现了**。
情形三:中间人改正文,顺手把 MD5 也重算一遍
收到时:正文 + md5=310de3bb1f169613(中间人算的)
本地重算 md5=310de3bb1f169613
结果:一致,**没发现**。校验和是公开算法,谁都能算。
情形四:换成带密钥的 HMAC-SHA256
正文没改:hmac=137f355e32caf080c0346a9b3b6a4d48
正文被改:hmac=45358e4e64215951231d55fd76bb0fcf
中间人拿错密钥重算:ceb05da1689e7b101300d4e593749763
本地用真密钥重算:45358e4e64215951231d55fd76bb0fcf
结果:对不上,**发现了**。中间人没有密钥,算不出那个值。
四行的分水岭在「密钥」上。没有密钥的校验和只能防「意外损坏」,
防不住「有人特意改」。而 TLS 恰恰让双方在握手阶段协商出一个
中间人不知道的密钥,于是每一条记录都带着一个别人伪造不了的校验值。
所以 7.1.3 说的「无法证明报文完整性」,不是「HTTP 没有校验和」——
它连校验和都可能是中间人算的。缺的不是算法,是那个只有双方知道的密钥。
两种正文都是 22 字节——改一个汉字不改变长度,所以连 Content-Length 都不会露馅。
情形二:发出时 md5 是 41240b03f1aa3449,收到时是 310de3bb1f169613,对不上,发现了。
情形三:中间人把 md5 也改成 310de3bb1f169613,接收方重算还是这个值,一致,没发现。
这一行就是「校验和不等于安全」。
情形四:正文没改时 hmac 是 137f355e...,改了是 45358e4e...;
中间人拿错密钥算出 ceb05da1...,与真值不符,发现了。
30 秒实验
- 把
secret换成空字节串,看 HMAC 还挡不挡得住。 - 试着只改正文里的一个字节(比如把「元」换成「块」),看长度变不变。
- 想一想:如果双方从没见面就通信,密钥要怎么安全地协商出来?
常见误区
- 「加了 MD5 就防篡改」——公开算法谁都能算,只能防意外损坏。
- 「完整性可以独立于加密实现」——没有密钥协商,完整性就是空话。
- 「改一个字长度会变,所以能发现」——同长度替换根本看不出来。
自测三问(点开对答案)
1. 为什么 MD5 挡不住篡改? —— 算法公开,中间人改完正文可以顺手重算,接收方无从分辨。 2. HMAC 和普通哈希的关键区别是什么? —— HMAC 需要密钥,没有密钥就算不出正确的校验值。 3. TLS 里完整性是怎么做的? —— 通常和加密合在一起(AEAD),解密时校验认证标签。三种风险摆完了。接下来看 HTTPS 是怎么用一层 TLS 把它们一次全解决的。
- ☐ (本节完,下一节 7.2.1 HTTP加上加密处理和认证以及完整性保护后即是HTTPS)