4. 签名数据预处理:哈希算法选择、数据编码与填充规则

签名验签这件事,很多人一上来就盯着算法本身。RSA 对不对?ECDSA 行不行?

但我跟你说,真正出问题的地方,往往不在算法核心,而在预处理阶段。哈希选错了、编码搞混了、填充规则没对齐——这些才是日常踩坑的重灾区。

我自己排查过的签名失败案例里,大概有六成以上都是预处理环节出的问题。所以这一章,咱们就把这块掰开揉碎了讲清楚。

核心观点:签名预处理决定了签名结果的正确性。哈希算法、数据编码、填充规则,三者必须与验证方完全一致。任何一处偏差,签名都会失败。

签名数据预处理流程 原始数据 数据编码 (Base64/Hex) 哈希计算 (SHA1/SHA256/SM3) 填充规则 (PKCS#1 v1.5/PSS) 常见错误 1. 编码方式不一致 2. 哈希算法不匹配 3. 填充模式选错 4. 数据未编码直接哈希 5. 哈希结果编码错误 避坑建议: 先确认对方用什么编码 再确认哈希算法 最后对齐填充规则

4.1 哈希算法选择:SHA1、SHA256 还是 SM3?

哈希算法是签名预处理的第一步。它的作用是把任意长度的数据,压缩成固定长度的摘要。签名算法真正签的是这个摘要,而不是原始数据。

那选哪个?

我个人的建议很简单:

  • SHA1:别用了。虽然很多老系统还在跑,但 SHA1 已经被证明存在碰撞攻击。2017 年 Google 就展示了两个不同 PDF 文件拥有相同 SHA1 哈希值。你想想看,如果签名能碰撞,那签名还有什么意义?
  • SHA256:目前最通用的选择。绝大多数现代签名算法都默认搭配 SHA256。RSA 2048 + SHA256 是业界标配。
  • SM3:国密标准。如果你的项目涉及国内合规要求,或者对接的是银行、政务系统,那 SM3 是必须的。SM3 的输出长度是 256 位,和 SHA256 一样,但算法不同。

我的经验:有一次对接某银行接口,对方文档写的是「SHA256 签名」。我按 SHA256 算完,怎么验都不过。折腾了两天,最后发现他们实际用的是 SM3,只是文档没更新。所以,不要只看文档,一定要拿一个已知正确的签名样本做验证。

哈希算法 输出长度 安全性 推荐场景
SHA1 160 位 已不安全 仅兼容遗留系统
SHA256 256 位 安全 通用场景,推荐
SM3 256 位 安全 国密合规场景

4.2 数据编码问题:Base64 还是 Hex?

哈希计算出来的结果是二进制数据。但签名和验签过程中,这些二进制数据需要传输、存储、比对。这时候就需要编码。

两种最常见的编码方式:

  • Hex(十六进制):每个字节用两个字符表示。比如 0xAB 变成 "ab"。可读性好,但体积翻倍。
  • Base64:每 3 个字节编码成 4 个字符。体积增加约 33%。更紧凑,但可读性差。

这里有个坑,我踩过不止一次:编码不一致。

比如签名方把哈希结果用 Base64 编码后传过去,验签方却用 Hex 解码。结果就是两边的字节流完全不一样,签名自然失败。

警告:编码问题是最隐蔽的错误之一。因为两边看起来都是「一串字符」,但实际含义完全不同。我曾经排查过一个线上故障,双方日志里打印的签名值看起来一模一样,但就是验不过。最后发现——一方打印的是 Hex,另一方打印的是 Base64,肉眼根本看不出来。

怎么避免?

  • 在接口文档里明确写明:签名前的数据用什么编码?签名结果用什么编码?
  • 调试时,把签名值和验签值的原始字节打印出来比对。不要只看字符串。
  • 我习惯在代码里加一个调试开关,输出签名全过程的中间结果:原始数据 → 编码后数据 → 哈希值 → 签名结果。这样一旦出问题,一眼就能定位。

4.3 数据填充规则:PKCS#1 v1.5 还是 PSS?

填充规则主要影响 RSA 签名。RSA 签名算法本身不直接对哈希值签名,而是先对哈希值做填充,再对填充后的结果做模幂运算。

两种主流填充模式:

  • PKCS#1 v1.5:传统模式。结构简单,兼容性好。但存在一些已知的理论攻击风险(虽然实际利用难度很高)。
  • PSS(Probabilistic Signature Scheme):更安全的模式。引入了随机数,使得同样的数据每次签名结果不同。推荐在新系统中使用。

这里有个容易混淆的地方:填充规则必须和验签方一致。你用 PKCS#1 v1.5 签名,对方用 PSS 验签,肯定失败。

关键点:很多签名库默认使用 PKCS#1 v1.5。如果你切换到 PSS,需要显式指定。我建议新项目直接用 PSS,安全性更好。但如果要对接老系统,先确认对方用什么。

代码示例(Java):

// PKCS#1 v1.5 签名
Signature sig = Signature.getInstance("SHA256withRSA");
sig.initSign(privateKey);
sig.update(data);
byte[] signature = sig.sign();

// PSS 签名
Signature sigPSS = Signature.getInstance("SHA256withRSA/PSS");
sigPSS.setParameter(new PSSParameterSpec(
    "SHA256", "MGF1", MGF1ParameterSpec.SHA256, 32, 1));
sigPSS.initSign(privateKey);
sigPSS.update(data);
byte[] signaturePSS = sigPSS.sign();

4.4 综合避坑指南

好了,上面三个环节都讲完了。最后我总结一下,预处理阶段最容易出问题的几个点:

  1. 哈希算法不匹配:签名方用 SHA256,验签方用 SM3。结果当然不对。
  2. 编码方式不一致:签名结果用 Base64,验签方用 Hex 解码。字节流完全错位。
  3. 填充规则没对齐:PKCS#1 v1.5 和 PSS 混用。
  4. 数据未编码直接哈希:有些场景要求先对数据做 Base64 编码,再对编码后的字符串做哈希。如果跳过编码直接哈希,结果就不同了。
  5. 哈希结果编码错误:哈希结果是二进制,需要编码后再签名。有些人直接把二进制字节数组传给签名函数,有些人先转 Hex 字符串再传。这两种做法结果完全不同。

我的调试习惯:每次遇到签名验签问题,我会先打印三个中间值:

  • 原始数据(确认输入正确)
  • 编码后的数据(确认编码方式一致)
  • 哈希值(确认哈希算法一致)

这三个值只要有一个对不上,后面的签名验签就不用看了。先解决预处理的问题,再谈算法本身。