📘 第六章 HTTP首部
📄 一句话概括

Content-MD5 是正文 MD5 摘要的 base64 写法,用来发现传输中的意外损坏。 它挡不住故意篡改,而且已经在 2014 年被规范删掉了。

📝 本节和前面几节的关系

上一节说正文的地址,这一节给正文配指纹。 它和 6.5.3 的 ETag 目的不同:那个比「变没变」,这个比「坏没坏」。

一、它长什么样

MD5 算出 16 字节,转成 base64 是 24 个字符,末尾常带 ==。 Demo 里 352 字节的正文,摘要就是 Kmvnlew45Q/itkhxR/k6g==。 客户端收到后自己算一遍,跟首部里的比:一致就收下,不一致就丢掉重传。 Demo 两组对照,正确值那组一致,硬编成全 A 的那组立刻判不一致。 注意它算的是编码之后的实体正文。 gzip 压过的,就按压缩后的字节算——这一点和 Content-Length 一致,和「原文」无关。

二、三个硬伤

第一,MD5 早就能造碰撞了。 攻击者可以构造出两个 MD5 相同、内容不同的东西,所以它只能防「意外损坏」,防不住「故意篡改」。 第二,它只覆盖正文,不覆盖首部。改个 Content-Type 或者 Location,校验和照样对得上。 第三,它要求接收方把整段正文收完才能校验,没法边收边验,对大文件来说是额外的内存和延迟成本。

三、为什么它被删了

2014 年的 RFC 7231 把 Content-MD5 从规范里移除了,理由就是上面那三条:算法不安全、覆盖面不够、 流式校验困难。 替代方案是 Digest 首部配 SHA-256,覆盖面也更大。 但更常见的做法是干脆用 ETag 兼任——反正内容一变指纹就变,一份数据干两件事。 所以今天看到 Content-MD5,基本可以判断这是个老系统。 它不算错,只是不再被推荐——真要做完整性校验,用更强的算法, 并且想清楚到底要防「传输错误」还是「恶意篡改」。

四、动手 Demo:算一遍,比一遍

服务端对 352 字节正文算 MD5 再 base64,分别发正确值和硬编的错值;客户端收到后自己再算一遍对照。

digest = base64.b64encode(hashlib.md5(body).digest()).decode('ascii')

def make(claim):
    def fn(raw, st):
        return X.reply(u'HTTP/1.1 200 OK',
                       [(u'Content-Type', u'text/plain'),
                        (u'Content-MD5', claim)], body)
    return fn

# 客户端侧
got = X.body_of(out)
mine = base64.b64encode(hashlib.md5(got).digest()).decode('ascii')
ok = (mine == claim)

实跑输出:

--------------------------------------------------------------------
Content-MD5:给正文算个校验和
--------------------------------------------------------------------
  正文 352 字节,MD5 的 base64 写法是:
    Kmvnlew45Q/itkhxR/k66g==(24 个字符)

  客户端收到后自己算一遍,再跟首部里那个比:
  首部声明                   本地算的                   判定
  正确值                     Kmvnlew45Q/itkhxR/k6..     一致,收下
  错的(全 A)               Kmvnlew45Q/itkhxR/k6..     不一致,该丢弃

  第二行就是它的全部价值:能发现传输过程中正文被改了或者截断了。
  但要说清三件事:
    一、它只保证「不是意外损坏」,挡不住故意篡改——MD5 早就能造碰撞了。
    二、它算的是**编码之后**的实体正文,gzip 压缩过的就按压缩后的字节算。
    三、2014 年的 RFC 7231 已经把它从规范里删掉了。

  现在要校验完整性,标准做法是 `Digest`(SHA-256),
  或者干脆用 ETag 兼任——反正内容一变指纹就变,一份数据干两件事。

正确值那组:本地算出 Kmvnlew45Q...,与首部一致,收下。 错值那组:本地算出的还是同一个值,与声明的全 A 不一致,该丢弃。 两组本地算出来的必然相同——因为正文没变。 差别只在「声明值」上,这正是校验和的用法:算的不变,比的变。 顺带看一眼长度:MD5 十六字节,base64 之后 24 个字符。 这 24 个字符要占掉首部里的一整行,而它能挡的东西越来越少。

30 秒实验

  1. 把正文改一个字节再对比,看摘要会不会变。
  2. 改成对 gzip 压缩后的字节算摘要,看值差多少。
  3. 想一想:为什么「只覆盖正文」是个安全隐患?

常见误区

  1. 「Content-MD5 能防篡改」——只能防意外损坏,MD5 可造碰撞。
  2. 「它算的是原文的摘要」——算的是编码后实体正文的摘要。
  3. 「它是现行标准」——2014 年 RFC 7231 已把它移除。
自测三问(点开对答案) 1. MD5 十六字节,base64 之后多长? —— 24 个字符。 2. 它算的是原文还是编码后的正文? —— 编码后的实体正文,和 Content-Length 口径一致。 3. 为什么现在不建议用它? —— MD5 可造碰撞、只覆盖正文、无法流式校验,2014 年已被规范移除。

校验和管「坏没坏」,下一个字段管「这一段在整份里的位置」——Content-Range。

相关笔记