数字签名技术:签名生成与验证流程
数字签名,说白了就是给数据盖个「电子公章」。它要解决三个问题——你是谁、你说了算不算、中途有没有人改过。我在做支付接口测试时,发现很多新人把签名和加密混为一谈,结果验签总失败。嗯,咱们先把概念理清楚。
为什么需要数字签名?
接口通信中,我们最怕什么?
- 身份伪造:别人冒充你的系统发请求。
- 数据篡改:请求体被中间人改了,你还在傻傻地处理。
- 抵赖:对方不承认自己发过那条指令。
数字签名能同时搞定这三件事。它不像加密那样要藏内容,而是把内容和一个不可否认的「指纹」绑在一起。我习惯类比成手写签名——你签了字,就不能赖账,别人也不能照葫芦画瓢。
签名与验签的底层逻辑
整个过程分两步:发方生成签名,收方验证签名。核心角色有三个:私钥(只有自己知道)、公钥(公开)、哈希函数(把任意长度变成固定长度摘要)。
- 签名方:用私钥对消息的哈希摘要进行签名,得到签名值。
- 验签方:用对应的公钥对签名值解密,并与消息的哈希摘要比对。
你看,私钥签名、公钥验签,这是不对称的。公钥能验证但不能伪造签名,这就是安全的基石。我在项目中曾经犯过一个低级错误——把私钥和公钥搞反了,愣是排查了两小时。所以后来我强制在配置里加了个公私钥后缀校验。
常见的签名算法:RSA vs ECDSA
实际项目中最常用的就两个:RSA 和 ECDSA。我用一张表对比它们的核心差别,这样你选型时心里有数。
| 特性 | RSA | ECDSA |
|---|---|---|
| 数学基础 | 大整数因子分解困难 | 椭圆曲线离散对数困难 |
| 密钥长度(安全等价) | 2048 位 | 256 位 |
| 签名生成速度 | 较慢(尤其大消息) | 较快 |
| 签名验证速度 | 较快 | 较慢于 RSA |
| 签名长度 | 固定 256 字节(2048位) | 固定 64 字节(P-256曲线) |
| 算法成熟度 | 极其成熟,兼容性最好 | 较新,但已被广泛接受 |
我个人习惯:如果对接的是老系统、银行类接口,优先选 RSA 2048。如果是移动端、IoT 或者对带宽敏感的场景,ECDSA 更合适,签名短、计算快。但要注意 ECDSA 的随机数质量,一旦随机数泄露就意味着私钥泄露——我见过一个案例就是因为随机数生成器没种子,导致两个签名用了相同随机数,直接被破解。
RSA 签名算法详解
RSA 签名流程相对直白:
- 对消息做哈希(比如 SHA-256),得到一个固定长度的摘要。
- 用私钥指数 d 对摘要进行模幂运算:
signature = hash^d mod n。 - 验签时用公钥指数 e 计算:
hash' = signature^e mod n,对比是否等于原始摘要。
关键点:RSA 签名不能直接对消息加密,必须先用哈希摘要。有些旧接口直接对消息做 PKCS#1 v1.5 签名,已经不安全了。建议使用 PSS 填充模式。
我的经验:RSA 签名长度和密钥长度挂钩。2048 位密钥的签名是 256 字节。我之前做过一个 HTTP Header 签名,忘了考虑 Base64 编码后的膨胀,结果 Header 超长被网关截断。所以用 RSA 时一定要算好签名后的传输开销。
ECDSA 签名算法详解
ECDSA 基于椭圆曲线,流程稍复杂但原理相似:
- 对消息 M 做哈希,得到摘要 e。
- 选取随机数 k,计算曲线点 (x1, y1) = k * G(G 是基点)。
- 计算 r = x1 mod n;用私钥 d 计算 s = k⁻¹ (e + r·d) mod n。
- 签名就是 (r, s) 对。
- 验签时用公钥 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 写好了,线上基本不会出幺蛾子。