web-dev-qa-db-ja.com

ECDSA署名からビット形式でRとSを抽出できますか?その逆も可能ですか?

フォーラムを検索して関連情報を見つけましたが、この質問には答えていません。私は、帯域幅が非常に制限された環境で作業しており、できるだけ小さなECDSA署名を作成する必要があります。今のところ、OpenSSLを使用しています。また、署名データは予測可能な長さでなければならないため、固定サイズです。

ECDSA署名は、曲線サイズに等しいビット長の2つの整数SとRで構成されていることを理解しています。さらに、cygwin wc -cコマンドからサイズを読み取って署名を作成しようとすると、6〜7バイトが追加されているようです。このオーバーヘッドが変化する可能性がある多くの場所を読みました。したがって、署名の長さの予測可能性が失われます。

考えられる解決策-SとRを抽出し、送信はバイナリ(?)形式です-ビットストリームと同じです。 OpenSSLライブラリとの互換性が失われると思われるため、これが可能かどうかはわかりません。したがって、プロセスを逆にして、OpenSSLが受け入れるエンコーディングでSとRを使用して署名を作成する必要があります。

OpenSSLに-binaryオプションがあり、空白のファイルが表示されることに気付きました。私は自分が間違った方向に進んでいると思いました。

3
BenM

古いことは知っていますが、最近同じ質問を見つけようとしばらく時間を費やしたので、ここにある種の答えがあります。

次のような署名を作成したとしましょう。

$ openssl dgst -sha256 -sign private_secp160k1.pem foo.txt > sign.bin

曲線は160ビットであるため、署名は2つの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バイトがあります。 DERまたはPEMでエンコードされた署名は、OpenSSLのasn1parseコマンドで検査して、バイトが何であるかを確認できます。

$ 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ヘッダーがあり、後続の要素のSEQUENCEがあり、合計の長さが45バイトであることを示しています。
  • バイト2には、長さ2のDERヘッダーがあり、SEQUENCEの最初の要素である長さ20のINTEGER要素を示します(20バイトは16進数で出力されます)
  • バイト24には、長さ2のDERヘッダーがあり、長さ21のINTEGER要素を示します。これはSEQUENCEの2番目の要素です(20バイトは16進数で出力されます)。

(d = Nは単に「深さ」を意味するため、2つの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

では、なぜ2番目の整数が21バイトなのですか?

DERの整数は2の補数であるため、20バイト整数の最初のバイトが> 0x7Fの場合、余分な0x00バイトが付加され、最初のビットは常に0になります。つまり、RとSは20バイトの数値ですが、それらをDER整数にエンコードすると、余分なバイトが必要になる場合があります。

OpenSSLでは、ECDSA_SIGタイプに2つの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 );
4
Frode