ECDSA 签名中的 Recovery 参数详解
1. ECDSA 签名的基本结构
ECDSA 签名包含两部分:
- r:32 字节
- s:32 字节
标准签名是 64 字节。在 Steem/Bitcoin 中,会在前面加一个 recovery 字节,形成 65 字节的紧凑签名(compact signature)。
[recovery_byte(1字节)] + [r(32字节)] + [s(32字节)] = 65字节
2. 为什么需要 Recovery 参数?
从签名恢复公钥时,需要知道:
- 使用哪个候选点(4 个可能)
- 公钥是压缩还是非压缩
Recovery 参数用于编码这些信息。
3. Recovery 参数的含义(0-3)
椭圆曲线上,给定 r 值,可能的 y 坐标有 2 个(y 和 -y),且可能使用 r 或 r+n(n 是曲线阶数),因此共有 4 种组合:
recovery = 0: 使用 r,y 是偶数
recovery = 1: 使用 r,y 是奇数
recovery = 2: 使用 r+n,y 是偶数
recovery = 3: 使用 r+n,y 是奇数
4. 压缩 vs 非压缩公钥
- 非压缩:33 字节(0x04 + x + y)
- 压缩:33 字节(0x02/0x03 + x,y 由奇偶性确定)
Steem 使用压缩格式。
5. Recovery 字节的编码方式
编码公式:
recovery_byte = base + compressed_flag + recovery_param
其中:
- base = 27(历史约定)
- compressed_flag = 0(非压缩)或 4(压缩)
- recovery_param = 0, 1, 2, 3
因此:
- 非压缩:27 + 0 + (0-3) = 27-30
- 压缩:27 + 4 + (0-3) = 31-34
6. 代码中的实现
C++ 代码(steem):
// 签名生成(总是使用压缩格式)
result.begin()[0] = 27 + 4 + recid; // 31-34
// 签名恢复
if (nV >= 31) {
// 压缩格式
EC_KEY_set_conv_form(my->_key, POINT_CONVERSION_COMPRESSED);
nV -= 4; // 去掉压缩标志
}
// 然后使用 nV - 27 作为 recovery 参数 (0-3)
dsteem:
// 期望 recovery 字节是 31-34
const recovery = buffer.readUInt8(0) - 31; // 得到 0-3
steem-js next版本(修复后):
// 生成签名时
const i = calcPubKeyRecoveryParam(...); // 得到 0-3
return new Signature(r, s, i + 31); // 编码为 31-34
// 解析签名时
const recoveryByte = buffer.readUInt8(0); // 31-34
const recovery = recoveryByte - 31; // 得到 0-3
7. 实际例子
假设:
- recovery 参数 = 1
- 使用压缩格式
那么:
recovery_byte = 27 + 4 + 1 = 32
签名格式:
[32] + [r的32字节] + [s的32字节]
恢复时:
recovery_byte = 32
compressed_flag = 32 >= 31 ? 4 : 0 // 是压缩的
recovery_param = 32 - 31 = 1 // 或者 (32 - 27) & 3 = 1
Upvoted! Thank you for supporting witness @jswit.