📘 第七章 确保Web安全的HTTPS
📄 一句话概括

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 秒实验

  1. 把 secret 换成空字节串,看 HMAC 还挡不挡得住。
  2. 试着只改正文里的一个字节(比如把「元」换成「块」),看长度变不变。
  3. 想一想:如果双方从没见面就通信,密钥要怎么安全地协商出来?

常见误区

  1. 「加了 MD5 就防篡改」——公开算法谁都能算,只能防意外损坏。
  2. 「完整性可以独立于加密实现」——没有密钥协商,完整性就是空话。
  3. 「改一个字长度会变,所以能发现」——同长度替换根本看不出来。
自测三问(点开对答案) 1. 为什么 MD5 挡不住篡改? —— 算法公开,中间人改完正文可以顺手重算,接收方无从分辨。 2. HMAC 和普通哈希的关键区别是什么? —— HMAC 需要密钥,没有密钥就算不出正确的校验值。 3. TLS 里完整性是怎么做的? —— 通常和加密合在一起(AEAD),解密时校验认证标签。

三种风险摆完了。接下来看 HTTPS 是怎么用一层 TLS 把它们一次全解决的。

相关笔记