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 秒实验
- 把正文改一个字节再对比,看摘要会不会变。
- 改成对 gzip 压缩后的字节算摘要,看值差多少。
- 想一想:为什么「只覆盖正文」是个安全隐患?
常见误区
- 「Content-MD5 能防篡改」——只能防意外损坏,MD5 可造碰撞。
- 「它算的是原文的摘要」——算的是编码后实体正文的摘要。
- 「它是现行标准」——2014 年 RFC 7231 已把它移除。
自测三问(点开对答案)
1. MD5 十六字节,base64 之后多长? —— 24 个字符。 2. 它算的是原文还是编码后的正文? —— 编码后的实体正文,和 Content-Length 口径一致。 3. 为什么现在不建议用它? —— MD5 可造碰撞、只覆盖正文、无法流式校验,2014 年已被规范移除。校验和管「坏没坏」,下一个字段管「这一段在整份里的位置」——Content-Range。
- ☐ (本节完,下一节 6.6.7 Content-Range)