数字签名技术:签名生成与验证流程

数字签名,说白了就是给数据盖个「电子公章」。它要解决三个问题——你是谁、你说了算不算、中途有没有人改过。我在做支付接口测试时,发现很多新人把签名和加密混为一谈,结果验签总失败。嗯,咱们先把概念理清楚。

为什么需要数字签名?

接口通信中,我们最怕什么?

  • 身份伪造:别人冒充你的系统发请求。
  • 数据篡改:请求体被中间人改了,你还在傻傻地处理。
  • 抵赖:对方不承认自己发过那条指令。

数字签名能同时搞定这三件事。它不像加密那样要藏内容,而是把内容和一个不可否认的「指纹」绑在一起。我习惯类比成手写签名——你签了字,就不能赖账,别人也不能照葫芦画瓢。

签名与验签的底层逻辑

整个过程分两步:发方生成签名,收方验证签名。核心角色有三个:私钥(只有自己知道)、公钥(公开)、哈希函数(把任意长度变成固定长度摘要)。

  1. 签名方:用私钥对消息的哈希摘要进行签名,得到签名值。
  2. 验签方:用对应的公钥对签名值解密,并与消息的哈希摘要比对。

你看,私钥签名、公钥验签,这是不对称的。公钥能验证但不能伪造签名,这就是安全的基石。我在项目中曾经犯过一个低级错误——把私钥和公钥搞反了,愣是排查了两小时。所以后来我强制在配置里加了个公私钥后缀校验。

数字签名生成与验证流程 发送方(签名) 原始消息 M 哈希摘要 H(M) 私钥签名 签名值 S 接收方(验签) 消息 M + 签名值 S + 公钥 使用公钥验证 S 通过 / 拒绝 私钥只有签名方持有 公钥公开,任何人都可验

常见的签名算法:RSA vs ECDSA

实际项目中最常用的就两个:RSA 和 ECDSA。我用一张表对比它们的核心差别,这样你选型时心里有数。

特性 RSA ECDSA
数学基础 大整数因子分解困难 椭圆曲线离散对数困难
密钥长度(安全等价) 2048 位 256 位
签名生成速度 较慢(尤其大消息) 较快
签名验证速度 较快 较慢于 RSA
签名长度 固定 256 字节(2048位) 固定 64 字节(P-256曲线)
算法成熟度 极其成熟,兼容性最好 较新,但已被广泛接受

我个人习惯:如果对接的是老系统、银行类接口,优先选 RSA 2048。如果是移动端、IoT 或者对带宽敏感的场景,ECDSA 更合适,签名短、计算快。但要注意 ECDSA 的随机数质量,一旦随机数泄露就意味着私钥泄露——我见过一个案例就是因为随机数生成器没种子,导致两个签名用了相同随机数,直接被破解。

RSA 签名算法详解

RSA 签名流程相对直白:

  1. 对消息做哈希(比如 SHA-256),得到一个固定长度的摘要。
  2. 用私钥指数 d 对摘要进行模幂运算:signature = hash^d mod n。
  3. 验签时用公钥指数 e 计算:hash' = signature^e mod n,对比是否等于原始摘要。

关键点:RSA 签名不能直接对消息加密,必须先用哈希摘要。有些旧接口直接对消息做 PKCS#1 v1.5 签名,已经不安全了。建议使用 PSS 填充模式。

我的经验:RSA 签名长度和密钥长度挂钩。2048 位密钥的签名是 256 字节。我之前做过一个 HTTP Header 签名,忘了考虑 Base64 编码后的膨胀,结果 Header 超长被网关截断。所以用 RSA 时一定要算好签名后的传输开销。

ECDSA 签名算法详解

ECDSA 基于椭圆曲线,流程稍复杂但原理相似:

  1. 对消息 M 做哈希,得到摘要 e。
  2. 选取随机数 k,计算曲线点 (x1, y1) = k * G(G 是基点)。
  3. 计算 r = x1 mod n;用私钥 d 计算 s = k⁻¹ (e + r·d) mod n。
  4. 签名就是 (r, s) 对。
  5. 验签时用公钥 Q 恢复出点,判断 r 是否匹配。

你看,ECDSA 签名里有个随机数 k。如果两次签名用了同一个 k,攻击者就能反推出私钥。这就是著名的「随机数复用攻击」。我记得有一次在测试环境用 ECDSA 签名,某 SDK 内部缓存了 k,结果连续签出相同签名,吓得我赶紧换库。

⚠️ 非常重要:ECDSA 的随机数生成器必须使用密码学安全伪随机数生成器(CSPRNG),不能用 rand() 或 system.currentTimeMillis() 当种子。曾经有个著名钱包因此丢了几百万美金。

实践中的踩坑与建议

做签名接口测试这么多年,坑没少踩,下面几个你大概率也会遇到:

  • 哈希算法不统一:签名方用 SHA-256,验签方误配成 SHA-1,结果肯定对不上。我们的做法是在接口文档里写明哈希算法和签名算法编号。
  • 参数排序问题:有些接口要求先对参数按字典序排序,再用 key=value 拼接后签名。漏了排序,签名永远不对。我习惯写个工具函数自动规范化。
  • 时间戳防重放:签名本身不防重放,需要加上时间戳或 nonce。但注意时间戳如果落在签名内容之外,中间人可以篡改。
  • 密钥轮换策略:私钥不能一成不变。我建议每 3-6 个月轮换一次,旧密钥保留一个过渡期,避免业务中断。

我曾经在一个金融项目中,因为签名验签的「字符编码」没统一(签名方用 UTF-8,验签方默认 GBK),导致 10% 的请求验签失败。排查了整整一天,最后发现是签名前的消息字符串编码不一致。从那以后,我所有的签名方案里都强制指定 UTF-8,并在文档里写死。

测试小贴士:写测试用例时,至少覆盖以下场景:正常签名验签通过、篡改消息体验签失败、使用错误公钥验签失败、重复发送旧签名(时戳失效)、签名值为空/异常类型时的异常处理。这些 Case 写好了,线上基本不会出幺蛾子。