Develop with pleasure!

福岡でCloudとかBlockchainとか。

署名に厳密なDERエンコーディングを適用したBIP-066

techmedia-think.hatenablog.com

でECDSAの署名データの改竄に遭遇したのもあり、関連して現在のBitcoinプロトコルに適用されている署名のエンコーディングルールについて定義したBIP-066の内容もおさえておく。

bips/bip-0066.mediawiki at master · bitcoin/bips · GitHub

動機

Bitcoinの参照実装では、署名検証をOpenSSLに依存しており(現在は独自実装したlibsecp256k1)、これがBitcoinのブロックの有効性を検証する暗黙的なルールになっている。しかし残念ながらOpenSSLはコンセンサスクリティカルな動作をするよう設計されておらず、その変更がBitcoinのソフトウェアに影響を及ぼす可能性がある。

特に重要な機能の1つが署名のエンコードである。最近までOpenSSLはDER標準から外れた署名であっても有効な署名として受け入れてきた。この仕様がOpenSSL 1.0.0p と 1.0.1kで変更されると、いくつかのノードがチェーンをリジェクトするようになった。

このBIPでは、コンセンサスルールがOpenSSLの署名解析に依存しないよう、DERが要求するものを正確に制限することを提案している。コンセンサスコードの実装から全てのOpenSSLを削除する場合、この変更が必要になる。

仕様

OP_CHECKSIGOP_CHECKSIGVERIFYOP_CHECKMULTISIGOP_CHECKMULTISIGVERIFYに渡される全ての署名は、ECDSAの検証がされる。これらの署名には後述する厳密なDERエンコーディングエンコードされなければならない。

上記opcodeは全て、公開鍵/署名のペアに対してECDSAの検証を実行する。この検証では、署名が以下のIsValidSignatureEncodingチェックをパスしないと、そのスクリプトは即falseと評価される。署名は有効なDERだけど、ECDSAの検証でエラーになった場合、opcodeはこれまでと同様opcodeの実行を停止しスタックにfalseをプッシュする(ただしこの場合スクリプトはすぐにエラーにはならない)。

DERエンコーディング仕様

以下のコードが厳密なDERチェックの内容になる。この関数は1バイトのsighashフラグを含む署名のbyte vectorをテストする(sighashフラグはDERの仕様外でこの提案の影響は受けない)。この関数は署名の長さが0の場合は呼び出されない。

bool static IsValidSignatureEncoding(const std::vector<unsigned char> &sig) {
    // フォーマット: 0x30 [total-length] 0x02 [R-length] [R] 0x02 [S-length] [S] [sighash]
    // * total-length: sighashを除くこの後に続くデータのデータ長を示す1バイトのデータ
    // * R-length: Rの長さを示す1バイトのデータ
    // * R: 任意の長さでビッグエンディアンでエンコードされたRの値。正の整数の最小エンコーディングになる。(次のバイト値が最上位bitでない限りnullバイトで始まってはならない。)
    // * S-length: Sの長さを示す1バイトのデータ
    // * S: 任意の長さでビッグエンディアンでエンコードされたRの値。(次のバイト値が最上位bitでない限りnullバイトで始まってはならない。)
    // * sighash: sihash typeを示す1バイトのデータ

    // データ長の最小・最大値チェック
    if (sig.size() < 9) return false;
    if (sig.size() > 73) return false;

    // A signature is of type 0x30 (compound).
    if (sig[0] != 0x30) return false;

    // 署名のトータルサイズを検証
    if (sig[1] != sig.size() - 3) return false;

    // Rの値の長さを取得
    unsigned int lenR = sig[3];

    // 残りの署名データにSの値が格納されている余地があるか検証
    if (5 + lenR >= sig.size()) return false;

    // Sの値の長さを取得
    unsigned int lenS = sig[5 + lenR];

    // 各要素の長さの合計が署名のデータサイズと一致するか検証
    if ((size_t)(lenR + lenS + 7) != sig.size()) return false;
 
    // Rの値がintegerか検証
    if (sig[2] != 0x02) return false;

    // Rの値の長さが0の場合はエラー
    if (lenR == 0) return false;

    // Rの値がマイナス値の場合はエラー
    if (sig[4] & 0x80) return false;

    // Rがマイナスの値として解釈されない限り、Rの先頭がnull byteは許可されない。
    if (lenR > 1 && (sig[4] == 0x00) && !(sig[5] & 0x80)) return false;

    // Sの値がintegerか検証
    if (sig[lenR + 4] != 0x02) return false;

    // Sの値の長さが0の場合はエラー
    if (lenS == 0) return false;

    //  Sの値がマイナス値の場合はエラー
    if (sig[lenR + 6] & 0x80) return false;

    // Sがマイナスの値として解釈されない限り、Sの先頭がnull byteは許可されない。
    if (lenS > 1 && (sig[lenR + 6] == 0x00) && !(sig[lenR + 7] & 0x80)) return false;

    return true;
}

表記:P1とP2はシリアライズされた公開鍵で、S1及びS2はP1とP2に対する有効な署名。S1' と S2' は非DERだがP1とP2の有効な署名。FはDER準拠の無効な署名で、F'はDERに準拠しない無効な署名。

  1. S1' P1 CHECKSIG fails (changed)
  2. S1' P1 CHECKSIG NOT fails (unchanged)
  3. F P1 CHECKSIG fails (unchanged)
  4. F P1 CHECKSIG NOT can succeed (unchanged)
  5. F' P1 CHECKSIG fails (unchanged)
  6. F' P1 CHECKSIG NOT fails (changed)
  7. 0 S1' S2 2 P1 P2 2 CHECKMULTISIG fails (changed)
  8. 0 S1' S2 2 P1 P2 2 CHECKMULTISIG NOT fails (unchanged)
  9. 0 F S2' 2 P1 P2 2 CHECKMULTISIG fails (unchanged)
  10. 0 F S2' 2 P1 P2 2 CHECKMULTISIG NOT fails (changed)
  11. 0 S1' F 2 P1 P2 2 CHECKMULTISIG fails (unchanged)
  12. 0 S1' F 2 P1 P2 2 CHECKMULTISIG NOT can succeed (unchanged)

デプロイメント

BIP-034と同じ2つの閾値(詳細はBIP-034参照)を使ったデプロイを利用する。ただし、ブロックのnVersionに3をセットする。1000ブロック中750ブロックのnVersionが3になるとこのルールは有効になる。さらに1000ブロック中950ブロックのnVersionが3になるとnVersionに2をセットしたブロックは無効になり、全てのブロックがこの新しいルールを採用する。

実際にBitcoin Core 0.10.0(2015年2月)で導入され、2015年7月時点で完全に有効になったみたい。

互換性

DERに厳密に準拠した署名を作成する本要件は、参照クライアントのv0.8.0以降でリレーポリシーとして実装され、違反するトランザクションは2015年1月時点でチェーンに追加されていない。さらに、準拠していない署名は準拠した署名に変換できるため、この要件によって機能が失われることはない。またこの提案にはトランザクションのmalleabilityを軽減するというメリットもある(詳細はBIP-062参照)。

実装

参照クライアントの実装↓

github.com

所感

署名の検証ルールがOpenSSL依存というのは確かに危ないので、明示的に検証ルールを定義するのは大事。