我搜索了论坛并找到了相关信息,但没有找到这个问题的答案。我在一个带宽极其受限的环境中工作,需要创建尽可能小的 ECDSA 签名。我们目前使用 OpenSSL。此外,由于签名数据的长度必须可预测,因此必须为固定大小。
我理解 ECDSA 签名由两个整数 S 和 R 组成,其位长等于曲线大小。此外,当我尝试创建签名并从cygwin wc -c
命令中读取大小时,似乎添加了 6-7 个字节。我读到过很多地方说这种开销可能会有所不同 - 因此我们失去了对签名长度的可预测性。
可能的解决方案 - 提取 S 和 R 并以二进制形式传输 - 就像比特流一样。我不确定这是否可行,因为我猜它失去了与 OpenSSL 库的兼容性。所以我们需要逆转这个过程,使用 OpenSSL 可以接受的编码中的 S 和 R 构建签名。
我注意到-binary
OpenSSL 中有一个选项会返回一个空白文件。我以为我走错了路。
答案1
我知道这已经很老了,但是我最近花了一些时间试图弄清楚同样的问题,所以这里有一个答案。
假设您已经创建了如下签名:
$ openssl dgst -sha256 -sign private_secp160k1.pem foo.txt > sign.bin
由于曲线为 160 位,因此签名是两个 20 字节的数字。默认情况下,OpenSSL 会将其写入 ASN.1 的二进制 DER 格式。检查它,我们应该至少找到 2 * 20 字节:
$ xxd -i < sign.bin
0x30, 0x2d, 0x02, 0x14, 0x22, 0xd0, 0x8b, 0xc1, 0x0d, 0x0b, 0x7b, 0xff,
0xe6, 0xc1, 0x77, 0xc1, 0xdc, 0xc4, 0x2f, 0x64, 0x05, 0x17, 0x71, 0xc8,
0x02, 0x15, 0x00, 0xdd, 0xf4, 0x67, 0x10, 0x39, 0x92, 0x1b, 0x13, 0xf2,
0x40, 0x20, 0xcd, 0x15, 0xe7, 0x6a, 0x63, 0x0b, 0x86, 0x07, 0xb6
在这个特定的 DER 编码签名中,有 47 个字节。可以使用 OpenSSL 的asn1parse
命令检查 DER 或 PEM 编码签名,以找出字节是什么:
$ openssl asn1parse -inform DER -in sign.bin
0:d=0 hl=2 l= 45 cons: SEQUENCE
2:d=1 hl=2 l= 20 prim: INTEGER :22D08BC10D0B7BFFE6C177C1DCC42F64051771C8
24:d=1 hl=2 l= 21 prim: INTEGER :DDF4671039921B13F24020CD15E76A630B8607B6
简而言之,它说:
- 在字节 0 处,有一个长度为 2 的 DER 头,表示后面有一个元素序列,总长度为 45 个字节。
- 在字节 2 处,有一个长度为 2 的 DER 标头,表示长度为 20 的 INTEGER 元素,即 SEQUENCE 的第一个元素(然后以十六进制打印 20 个字节)
- 在字节 24 处,有一个长度为 2 的 DER 头,表示长度为 21 的 INTEGER 元素,即 SEQUENCE 的第二个元素(然后以十六进制打印 20 个字节)
(d=N 仅表示“深度”,因此两个 INTEGER 元素具有 d==1,因为它们是序列的一部分。)
通过漂亮地打印之前的原始字节,我们可以识别出这些元素:
$ xxd -i < sign.bin
0x30, 0x2d, # Sequence of length 45 to follow (45 == 0x2d)
0x02, 0x14, # Integer of length 20 to follow (20 == 0x14)
# Here come the 20 bytes:
0x22, 0xd0, 0x8b, 0xc1, 0x0d, 0x0b, 0x7b, 0xff, 0xe6, 0xc1,
0x77, 0xc1, 0xdc, 0xc4, 0x2f, 0x64, 0x05, 0x17, 0x71, 0xc8,
0x02, 0x15, # Integer of length 21 to follow (21 == 0x15)
0x00, # The first of the 21 integer bytes (see explanation below!)
# Here come the remaining 20 bytes
0xdd, 0xf4, 0x67, 0x10, 0x39, 0x92, 0x1b, 0x13, 0xf2, 0x40,
0x20, 0xcd, 0x15, 0xe7, 0x6a, 0x63, 0x0b, 0x86, 0x07, 0xb6
那么为什么第二个整数是21个字节?
DER 中的整数是二进制补码,因此如果 20 字节整数的第一个字节 > 0x7F,则会在前面添加一个额外的 0x00 字节,这样第一位始终为 0。换句话说,虽然 R 和 S 是 20 字节的数字,但将它们编码为 DER 整数可能需要一个额外的字节。
在 OpenSSL 中,ECDSA_SIG 类型包含两个 BIGNUM。因此,一旦您使用 解码了 DER 字节d2i_ECDSA_SIG
,您就可以使用 和 访问它们yourSig->r
,yourSig->s
这将小于等于 20 个字节(对于 160 位曲线)。
如果 R 或 S 的第一个最高有效数字恰好为零,则它们也可能需要少于 20 个字节。您可以使用 找到每个所需的字节数BN_num_bytes
,然后使用 将它们写入常规字节数组以BN_bn2bin
进行序列化。
ECDSA_SIG* ecSig = d2i_ECDSA_SIG(NULL, derBuf, derBufSize);
int rBytes = BN_num_bytes(ecSig->r), // Number of bytes needed for R
sBytes = BN_num_bytes(ecSig->s); // Number of bytes needed for S
// ...
unsigned char someBuffer[40] = {0};
BN_bn2bin( ecSig->r, someBuffer + 20 - rBytes ); // Place R first in the buffer
BN_bn2bin( ecSig->s, someBuffer + 40 - sBytes ); // Place S last in the buffer
在接收端,您只需通过设置其r
和s
成员来重新创建 ECDSA_SIG 实例:
ECDSA_SIG* ecSig = ECDSA_SIG_new();
BN_bin2bn( someBuffer, 20, ecSig->r );
BN_bin2bn( someBuffer + 20, 20, ecSig->s );