bitcoin-rubyのP2PKHの署名のデフォルト実装
bitcoin-rubyを使ってP2PKHのUTXOを入力にしたトランザクションの署名は↓のように書ける。
require 'bitcoin' Bitcoin.network = :testnet prev_key = Bitcoin::Key.from_base58('秘密鍵') prev_tx = Bitcoin::Protocol::Tx.new('入力のOutPointのTxのrawデータ'.htb) tx = Bitcoin::Protocol::Tx.new tx.add_in(Bitcoin::Protocol::TxIn.from_hex_hash(prev_tx.hash, 1)) tx.add_out(Bitcoin::Protocol::TxOut.value_to_address(bitcoinの量, '送り先のアドレス')) # 署名対象のsighash sig_hash = tx.signature_hash_for_input(0, prev_tx) # 署名の生成 sig = prev_key.sign(sig_hash) + [Bitcoin::Script::SIGHASH_TYPE[:all]].pack("C") tx.in[0].script_sig = Bitcoin::Script.new(Bitcoin::Script.pack_pushdata(sig) + Bitcoin::Script.pack_pushdata(prev_key.pub.htb)).to_payload # 署名済みのトランザクションのペイロード puts tx.to_payload.bth
署名処理の中身はBitcoin::Key#signで、実態はBitcoin#sign_dataの↓のコード
def sign_data(key, data) sig = nil loop { sig = key.dsa_sign_asn1(data) sig = if Script.is_low_der_signature?(sig) sig else Bitcoin::OpenSSL_EC.signature_to_low_s(sig) end buf = sig + [Script::SIGHASH_TYPE[:all]].pack("C") # is_der_signature expects sig + sighash_type format if Script.is_der_signature?(buf) break else p ["Bitcoin#sign_data: invalid der signature generated, trying again.", data.unpack("H*")[0], sig.unpack("H*")[0]] end } return sig end
ECDSAの署名自体はffiを使ってOpenSSLに委譲しており、この時生成される署名データは生成の都度、別の値になる。(なので非witnessな場合、txidも毎回変わる)
ECDSAの署名は、署名対象のデータと秘密鍵とランダムに生成されたnonceを使って生成される。OpenSSLでは署名の都度このnonceをランダムに生成しているため、↑のコードで同じトランザクションに署名をした際に、毎回異なる署名が生成される。
この時大事なのがランダムなnonce生成器の精度。nonce生成の実装アルゴリズムに偏りがあるとnonceが重複しやすく、同じnonceを使っている署名データから容易に秘密鍵を計算することができてしまうというリスクがある。
(そもそも真の乱数を生成するのは難しいし、偏りのない乱数かテストするのも難しい。)
RFC 6979とBitcoin CoreのSecp256k1
実装のミスなどによって偏りのあるnonceが生成されるような事を防ぐために、このnonceの生成処理を決定論的に行おうというのがRFC 6979↓
RFC 6979の3.2章にnonceの生成プロセスが記載されている↓
- 署名対象のデータ
m
のハッシュ値h1 = H(m)
を生成する(h1はhlenビットのシーケンス) - 32バイト分
0x01
をセットしたバイトシーケンスV
を作成 - 32バイト分
0x00
をセットしたバイトシーケンスK
を作成 K = HMAC_K(V || 0x00 || int2octets(x) || bits2octets(h1))
を計算
(||
は連結、x
は秘密鍵)V = HMAC_K(V)
を計算K = HMAC_K(V || 0x01 || int2octets(x) || bits2octets(h1))
を計算V = HMAC_K(V)
を計算- 空のシーケンス
T
し、その長さをtlen
とする。tlen < qlen
の間以下を繰り返す。
V = HMAC_K(V)
T= T || V
(qlen
はカーブオーダーの除数である十分に大きな素数q
の長さ)
Bitcoin Coreはsecp256k1を独自に実装してこの仕様を導入しているため、同じトランザクションデータと秘密鍵からは同じnonceが生成される。そのため同じトランザクションデータに対してsignrawtransaction
を何回実行しても、生成される署名データは同じものになる。
Bitcoin CoreでRFC 6979に基づいてnonceを生成して署名しているのが
https://github.com/bitcoin/bitcoin/blob/v0.13.2/src/secp256k1/src/secp256k1.c#L310-L389
と
https://github.com/bitcoin/bitcoin/blob/v0.13.2/src/secp256k1/src/hash_impl.h#L205-L270
あたり。
引数で与えられた秘密鍵と署名対象のデータが異なる場合、生成されるnonceは必ず別の値になる。
bitcoin-rubyで決定性署名の生成
bitcoin-rubyにもffiでsecp256k1を使用するインターフェースがあるので、署名の生成にsecp256k1を使うようにすれば決定性署名の生成が行える。
(secp256k1の関数を利用するには、環境変数にlibsecp256k1.soのパスをセットする必要がある。)
require 'bitcoin' Bitcoin.network = :testnet # libsecp256k1.soのパスを環境変数にセット ENV['SECP256K1_LIB_PATH'] = '/usr/local/lib/libsecp256k1.so' prev_key = Bitcoin::Key.from_base58('秘密鍵') prev_tx = Bitcoin::Protocol::Tx.new('入力のOutPointのTxのrawデータ'.htb) tx = Bitcoin::Protocol::Tx.new tx.add_in(Bitcoin::Protocol::TxIn.from_hex_hash(prev_tx.hash, 1)) tx.add_out(Bitcoin::Protocol::TxOut.value_to_address(bitcoinの量, '送り先のアドレス')) # 署名対象のsighash sig_hash = tx.signature_hash_for_input(0, prev_tx) # secp256k1を使った署名の生成 sig = Bitcoin::Secp256k1.sign(sig_hash, prev_key.priv.htb) + [Bitcoin::Script::SIGHASH_TYPE[:all]].pack("C") tx.in[0].script_sig = Bitcoin::Script.new(Bitcoin::Script.pack_pushdata(sig) + Bitcoin::Script.pack_pushdata(prev_key.pub.htb)).to_payload # 署名済みのトランザクションのペイロード puts tx.to_payload.bth
同じsecp256k1を使って決定性署名を生成しているため、↑で生成した署名済みトランザクションのペイロードと、署名前のトランザクションのペイロードをBitcoin Coreのsignrawtransaction
に投げて生成されるトランザクションのペイロードは同じ値になる。
※ 現在のbitcoin-rubyのバージョンは0.0.10で、それに対応したsecp256k1ライブラリはBitcoin Core v 0.13.1以上。またsecp256k1をビルドする際は、configureのオプションに--enable-module-recovery
を指定する必要がある。
OpenSSLではまだRFC 6979は実装されてないみたいなので、署名をする際はsecp256k1を使った方が安全かもしれない。