我能否以位形式从 ECDSA 签名中提取 R 和 S,反之亦然?

Ben*_*enM 6 openssl dsa

我搜索了论坛并找到了相关信息,但没有回答这个问题。我在一个带宽极度受限的环境中工作,需要创建尽可能小的 ECDSA 签名。目前,我们正在使用 OpenSSL。此外 - 由于签名数据必须具有可预测的长度 - 固定大小。

我知道 ECDSA 签名由两个整数 S 和 R 组成,位长等于曲线大小。此外,当我尝试创建签名,从cygwin wc -c命令读取大小时,似乎添加了 6-7 个字节。我已经阅读了很多地方,这种开销可能会有所不同 - 因此我们失去了签名长度的可预测性。

可能的解决方案 - 提取 S 和 R 并以二进制(?)形式传输 - 就像位流一样。我不确定这是否可行,因为我猜它失去了与 OpenSSL 库的兼容性。所以我们需要颠倒这个过程,在 OpenSSL 接受的编码中使用 S 和 R 构造一个签名。

我注意到-binaryOpenSSL中有一个选项,它给了我一个空白文件。我以为我走错了路。

Fro*_*ode 6

我知道它已经过时了,但我最近花了一些时间试图找出同样的问题,所以这里有各种各样的答案。

假设您已经创建了这样的签名:

$ openssl dgst -sha256 -sign private_secp160k1.pem foo.txt > sign.bin
Run Code Online (Sandbox Code Playgroud)

因为曲线是 160 位,所以签名是两个 20 字节的数字。默认情况下,OpenSSL 将以 ASN.1 的二进制 DER 格式编写它。检查它,我们应该找到至少 2 * 20 个字节:

$ openssl dgst -sha256 -sign private_secp160k1.pem foo.txt > sign.bin
Run Code Online (Sandbox Code Playgroud)

在这个特定的 DER 编码签名中,有 47 个字节。可以使用 OpenSSL 的asn1parse命令检查 DER 或 PEM 编码的签名,以找出字节是什么:

$ 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
Run Code Online (Sandbox Code Playgroud)

简而言之,它说:

  • 在字节 0 处,有一个长度为 2 的 DER 标头,表明后面有一个元素序列,总长度为 45 个字节。
  • 在字节 2 处,有一个长度为 2 的 DER 头,指示长度为 20 的 INTEGER 元素,即 SEQUENCE 的第一个元素(然后以十六进制打印 20 个字节)
  • 在字节 24 处,有一个长度为 2 的 DER 标头,指示长度为 21 的 INTEGER 元素,即 SEQUENCE 的第二个元素(然后以十六进制打印 20 个字节)

(d=N 仅表示“深度”,因此两个 INTEGER 元素具有 d==1,因为它们是序列的一部分。)

漂亮地打印之前的原始字节,可以识别元素:

$ 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
Run Code Online (Sandbox Code Playgroud)

那么为什么第二个整数是 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进行序列化。

$ 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
Run Code Online (Sandbox Code Playgroud)

在接收端,您只需通过设置其rs成员来重新创建 ECDSA_SIG 实例:

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
Run Code Online (Sandbox Code Playgroud)