验证 XML 数字签名不只是比一个摘要:Reference、SignedInfo、规范化、公钥来源、被签名元素的位置,每一步都可能出问题。这篇用 JDK 的 XMLSignatureFactory 逐项真实运行,包括签名包装攻击与默认的 secureValidation。
签名后文档 997 字节(原文 77 字节);Signature 元素放在根元素末尾(enveloped) Signature 里的算法:[2001/10/xml-exc-c14n#, 2001/04/xmldsig-more#rsa-sha256, 2000/09/xmldsig#enveloped-signature, 2001/04/xmlenc#sha256] 原样验证(用可信的公钥)→ 整体验证 = true(SignatureValue true,各 Reference [true]) 把 amount 改成 99999 → 整体验证 = false(SignatureValue true,各 Reference [false]) 把 note 改一个字符 → 整体验证 = false(SignatureValue true,各 Reference [false]) 改动 SignatureValue 的第一个字符 → 整体验证 = false(SignatureValue false,各 Reference [true]) 用另一把公钥验证原文 → 整体验证 = false(SignatureValue false,各 Reference [true]) 把整个 Signature 元素删掉 → 文档里没有 Signature 元素(应用必须把「没有签名」当作失败,不能当成「没有被篡改」)
签名文档的根元素:<order a="1" id="o1" z="3"> 原样 → 整体验证 = true(SignatureValue true,各 Reference [true]) 只调换根元素属性的顺序 → 整体验证 = true(SignatureValue true,各 Reference [true])(规范化把属性排序,仍然通过) 属性引号从双引号改成单引号 → 整体验证 = true(SignatureValue true,各 Reference [true]) 标签内属性之间的空格数量变化 → 整体验证 = true(SignatureValue true,各 Reference [true]) 在元素之间插入换行与缩进(元素之间的空白文本节点)→ 整体验证 = false(SignatureValue true,各 Reference [false])(空白文本节点也是签名内容的一部分) <item/> 改成 <item></item> → 整体验证 = true(SignatureValue true,各 Reference [true])(规范化后两者相同) 数字字符引用 1 代替字符 1 → 整体验证 = true(SignatureValue true,各 Reference [true]) 先解析再序列化一遍(同一份内容)→ 整体验证 = true(SignatureValue true,各 Reference [true])
攻击者用自己的密钥对伪造的文档签名,并把自己的公钥放在 KeyInfo/KeyValue 里;验证方用文档里的 KeyValue 取公钥 → 整体验证 = true(SignatureValue true,各 Reference [true])(通过!签名只能证明「谁持有这把私钥签的」,公钥必须是事先信任的) 同一份伪造文档,验证方使用事先信任的公钥 → 整体验证 = false(SignatureValue false,各 Reference [true]) 合法签名者(带 KeyValue)+ 信任列表里的公钥 → 整体验证 = true(SignatureValue true,各 Reference [true]) 验证方应当做的:把 KeyInfo 里的公钥与自己信任的公钥(或证书链)比较;这里 attacker 公钥与可信公钥是否相同 = false
正常签名文档,验证时注册 id 属性 → 整体验证 = true
签名包装攻击后:真正被签名的 order 被挪进了 <extra>,一个没有签名的伪造 order(to=mallory,amount=99999)放在最前面;验证 → 整体验证 = true
应用用 getElementsByTagName("order").item(0) 取「第一个 order」得到 to = mallory,amount = 99999(验证通过,但处理的不是被签名的那个元素)
正确做法:只处理被 Reference 实际指向的元素(按 ID 取):to = alice,amount = 100;并要求该元素在文档中的位置符合预期(这里它的父元素是 extra,预期是 root)
对照:正常文档里被签名元素的父元素 = rootRSA-SHA1 + SHA-1 摘要的签名,secureValidation = true(默认安全模式)→ MarshalException: It is forbidden to use algorithm http://www.w3.org/2000/09/xmldsig#rsa-sha1 when secure validation is enabled 同一份签名,secureValidation = false → 整体验证 = true(SignatureValue true,各 Reference [true]) RSA-SHA256 + SHA-256,secureValidation = true → 整体验证 = true(SignatureValue true,各 Reference [true]) 512 位 RSA 密钥(KeyValue 里)+ SHA-256,secureValidation = true → XMLSignatureException: javax.xml.crypto.dsig.XMLSignatureException: RSA keys less than 1024 bits are forbidden when secure validation is enabled;secureValidation = false → 整体验证 = true(SignatureValue true,各 Reference [true]) 1024 位 RSA 密钥(KeyValue 里)+ SHA-256,secureValidation = true → 整体验证 = true(SignatureValue true,各 Reference [true]);secureValidation = false → 整体验证 = true(SignatureValue true,各 Reference [true])