在《图解 HTTP》(上野 宣 著,于均良 译,人民邮电出版社,2014年4月,ISBN 9787115351531)第129页(6.6.6节),作者给出了一则首部示例:

Content-MD5: OGFkZDUwNGVhNGY3N2MxMDIwZmQ4NTBmY2IyTY==

和广为流传的例子 Content-MD5: Q2hlY2sgSW50ZWdyaXR5IQ== 比较一下,
大~家~有~没~有~觉~得~有~点~长?

第129页 第130页 部分

MD5 作为校验码,是一个 128 位长的二进制数。 在内存中,128 bits = 16 octets。 经过 Base64 编码,长度增加约 33%,编码后的长度应为 4*⌈16/3⌉ = 24 字节。 而书中栗子的长度为 40 字节。

原来这里也有个坑!

算法细节

HTTP/1.1(RFC2616#14.15)给出了实体首部字段 Content-MD5 的语法规则:

Content-MD5   = "Content-MD5" ":" md5-digest
md5-digest   = <base64 of 128 bit MD5 digest as per RFC 1864>

即校验值的编码依据为 RFC1864

MD5 算法输出的结果为 128 位长的摘要。当以网络字节序(大端序) 解析时,可得到16字节的二进制数据序列。随后,将这 16 个字节按 base64 算法编码,最终得到可作为 `Content-MD5` 字段取值的结果。因此,针对 MIME 实体的原始数据应用 MD5 算法,若得到的摘要值为(几乎不可能的)"Check Integrity!"注 1,则该 MIME 实体的首部可包含此字段:
Content-MD5:  Q2hlY2sgSW50ZWdyaXR5IQ==注 2
  1. 这是一个16字节长的字符串,对应的 32 字符表示为 0x436865636b20496e7465677269747921。(笔者查询了多个逆向库,均尚未收录 md5 为该值的数据。)

  2. “Check Integrity!”作为 ASCII 字符串的 base64 编码,长度为 24 字节。

看吧,RFC1864 给出的例子也是 24 字节长。 那么问题出在哪里?

刚才点击了 Base64 链接的读者可能已经通过实验发现答案了:

书中给出的字段值 OGFkZDUwNGVhNGY3N2MxMDIwZmQ4NTBmY2IyTY== 是对字符串 "8add504ea4f77c1020fd850fcb2M"的 base64 编码,而不是对二进制数 0x8add504ea4f77c1020fd850fcb2? 的 base64 编码。

注:实际解码过程中出现了错误(所以出现了比较显眼的“M”字母)。该 28 位结果是报错前的部分输出,但不影响分析问题。

在实践中,应注意先取得 md5 校验值的二进制表示,然后进行 base64 编码。例如:

  1. PHP 中使用 md5($数据, true) 而非 md5($数据) (via StackOverflow

  2. ColdFusion 中使用 binaryDecode(校验码字符串, "hex") 再用 binaryEncode(二进制数据, "base64") (via Ben Nadel

  3. Java 中使用 DigestInputStream.getMessageDigest().digest() 而不是隐式调用 DigestInputStream.getMessageDigest().toString() (via Amazon MWS

补充知识

字段名大小写

  1. HTTP/1.1(RFC2616#4.2)说了,字段名不区分大小写。所以即使写成 cOnTeNt-mD5 也不会有什么问题。

  2. 作为 HTTP/2 实验场的 SPDY 协议(最新草案为 3.2)规定,所有字段名均必须小写。

一定要用 base64 编码吗?

目前使用 Content-MD5 首部的大流量服务商只有两家:亚马逊和百度。(没错!) 虽然我把百度列在后面,并不意味着百度云的流量比亚马逊云小。 在互联网这个世界,的确是谁的嗓门大谁决定行业准则。

亚马逊严格遵守 RFC1864,不仅在下载时提供校验值,还对上传文件的校验值和请求中的值进行二次核对。

百度云网盘更贴近民生,直接在响应报文中返回 md5 校验码的十六进制表示(32 字节),看起来多方便呀!

ETag: b0d95dbfeeb97fa7e411aba81729229f
Content-MD5: b0d95dbfeeb97fa7e411aba81729229f

百度云网盘部分响应报文

32 只比 24 多 8 个字节,意义大不一样。何必死死遵守那个并不是为 HTTP 设计的方案呢?

(如果 Firefox 等保皇派拒绝兼容,给字段名加一个 X- 前缀得了。)