Develop with pleasure!

福岡でCloudとかBlockchainとか。

bitcoin-rubyで決定性署名を生成する

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 - Deterministic Usage of the Digital Signature Algorithm (DSA) and Elliptic Curve Digital Signature Algorithm (ECDSA)

RFC 6979の3.2章にnonceの生成プロセスが記載されている↓

  1. 署名対象のデータmハッシュ値 h1 = H(m)を生成する(h1はhlenビットのシーケンス)
  2. 32バイト分0x01をセットしたバイトシーケンスVを作成
  3. 32バイト分0x00をセットしたバイトシーケンスKを作成
  4. K = HMAC_K(V || 0x00 || int2octets(x) || bits2octets(h1)) を計算
    ||は連結、x秘密鍵
  5. V = HMAC_K(V)を計算
  6. K = HMAC_K(V || 0x01 || int2octets(x) || bits2octets(h1))を計算
  7. V = HMAC_K(V)を計算
  8. 空のシーケンス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を使った方が安全かもしれない。