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_CHECKSIG
、OP_CHECKSIGVERIFY
、OP_CHECKMULTISIG
、OP_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に準拠しない無効な署名。
S1' P1 CHECKSIG
fails (changed)S1' P1 CHECKSIG NOT
fails (unchanged)F P1 CHECKSIG
fails (unchanged)F P1 CHECKSIG NOT
can succeed (unchanged)F' P1 CHECKSIG
fails (unchanged)F' P1 CHECKSIG NOT
fails (changed)0 S1' S2 2 P1 P2 2 CHECKMULTISIG
fails (changed)0 S1' S2 2 P1 P2 2 CHECKMULTISIG NOT
fails (unchanged)0 F S2' 2 P1 P2 2 CHECKMULTISIG
fails (unchanged)0 F S2' 2 P1 P2 2 CHECKMULTISIG NOT
fails (changed)0 S1' F 2 P1 P2 2 CHECKMULTISIG
fails (unchanged)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参照)。
実装
参照クライアントの実装↓
所感
署名の検証ルールがOpenSSL依存というのは確かに危ないので、明示的に検証ルールを定義するのは大事。