什么是XML规范化(Canonicalization)

XML规范化是将逻辑等价的XML转换为唯一确定字节序列的过程,用于数字签名等需字节级一致性的场景;它通过强制换行、属性排序、编码统一等规则消除语法冗余差异。

XML规范化(Canonicalization)是把逻辑等价的XML文档,转换成**唯一、确定的字节序列**的过程。它不是美化格式,也不是简化结构,而是为数字签名、安全比对、回归测试等场景提供“可重复、可验证”的字节级一致性保障。

为什么XML必须规范化?

因为XML语法允许太多合法但不一致的写法: 在语义上完全一样,但字符串比较直接失败;Español 用 ISO-8859-1 编码和 UTF-8 编码输出的字节完全不同;换行符是 \r\n 还是 \n、属性值引号用单还是双、命名空间前缀是否省略……这些都让“内容没改,签名却验不过”成为高频故障。

Canonical XML 规范做了哪些关键标准化?

W3C 定义的 Inclusive Canonicalization(常用算法)会强制执行以下规则:

  • 所有换行统一为 \n(U+000A)
  • 属性按名称字母序重排(无视原始顺序)
  • 所有属性值进行空白归一化( a b a b),但保留标签内文本中的有效空白
  • 删除 XML 声明()、DTD、处理指令、注释
  • 展开实体引用(如   ),CDTA 内容转义为字符
  • 空元素统一为开始+结束标签形式(
  • 命名空间声明只保留必需的,冗余声明被剔除
  • 强制使用 UTF-8 编码输出(不依赖源文件编码)

Java 中用 TransformService 做规范化要注意什么?

这是最常出错的环节。很多开发者直接调用 TransformService.getInstance(CanonicalizationMethod.INCLUSIVE, "DOM"),却忽略几个隐性陷阱:

  • 输入 XML 必须是 well-formed,不能含未声明的实体(如 &lsb;)或外部 DTD 引用,否则解析阶段就抛异常
  • OctetStreamData 的输入流需确保是 UTF-8 字节流;若原始是 GBK 或 ISO-8859-1,先 decode 再 re-encode 成 UTF-8,否则规范化结果乱码
  • Android 上 org.apache.xml.securityCanonicalizer 可能因 DOM builder 默认校验失败而崩溃,建议改用标准 JAXP 实现
  • 不要对已嵌入 的 XML 全文规范化——签名验证时, 子树才需要规范化,其余部分(如 )必须原样保留

验证签名时规范化失败的典型现象

常见报错或静默失败表现:

  • SignatureException: Signature verification failed —— 90% 源于双方使用的规范化方法不一致(比如一方用 Inclusive,另一方误配 Exclusive
  • 本地计算的 DigestValue 和签名中记录的值始终不匹配,但 XML 看起来“一模一样”
  • 在 Windows 开发环境能过,在 Linux CI 流水线里失败——根源往往是换行符或默认字符集差异
  • 用浏览器打开 XML 显示正常,但程序解析后规范化输出多出空格或换行,导致哈希错位

规范化不是“选个算法跑一下”就完事的事。它要求签名方和验证方在协议层明确约定:用哪种 CanonicalizationMethod、输入是否预清理、命名空间是否显式声明、是否处理 xml:base 等边缘行为。漏掉任一细节,签名就变成不可靠的装饰品。