『ゼロから創る暗号通貨』
第6章: SimpleBitcoin のセキュリティに関する考察
濵津 誠/hamatz
前章までの実装では、説明のための簡易化を優先させた部分もあり、一般的な暗号通貨よりもセキュリティ的に弱い面が残っています。本章の目的は、そのような箇所を洗い出し、現在の実装の課題を認識することです。次のような点について考察することで、できあがったものを一般公開する前の注意点を確認しておきましょう。
「ひととおり動くものができあがったところで、ここからは、現状の実装を見ながらSimpleBitcoinのセキュリティについて確認していこうか」
「作りやすさを優先してシンプルなコードで作ってきたから、当然、暗号通貨としてセキュリティが弱いところがあるよねー」
「そうだね。もちろん、そういう面はある。それだけでなく、SimpleBitcoinとして目指すと決めた完成イメージを実現するため、あえて通常の暗号通貨とは違う考えで作ってきた部分もある」
「どういうこと?」
「SimpleBitcoinでは、サービス横断的に、あらゆるものに量的概念を持つ『いいね!』を付与することを目的としていただろう?そして、誰が一番『いいね!』を集めたかという、ランキングのようなものも想定していた」
「それは覚えているけど、それが通常の暗号通貨とどう違うの?」
「一般の暗号通貨では、誰が誰に送金したのかという情報は、あまり追跡できないように考えられてる」
「そうか。SimpleBitcoinには匿名性という観点がないんだね」
「そうだ。厳密にいうと、SimpleBitcoinのレイヤーより1つ上のところでアドレスとユーザーを紐づけるような処理を入れるわけだから、SimpleBitcoinの特性ではないのだけどね」
「それでも、その特性は把握しておく必要がある?」
「そういうことだ。大きくまとめると次の3つが要点といえる」
「なるほど。特に1は、何も考えずにサーバーのIPアドレスとポート番号をうっかりTweetしたりしてしまう前に確認しておかないと、大変なことになりそうだよね」
「そうだ。これまでと違ってコードをガリガリと書く話は少なくなるが、大事なポイントなのでしっかり確認してほしい」
「わかったよ、兄さん!」
「それではまず、もっとも大事な話として、サーバーを公開する前の注意事項を確認しておこう」
「意図どおりに動くことだけに集中してきたことで取りこぼしていた部分だね」
「それじゃあ、質問しよう。SimpleBitcoinのP2Pネットワークにおいて、攻撃されて困る部分はどこにあると思う?」
「P2Pネットワークの参加者の中に悪意の人が紛れ込んだ場合にされて困ることは何かってことだよね。うーん、なんだろう……」
「ちょっと難しかったかな。質問を変えよう。P2Pネットワークが成り立つために肝となっている部分は何だったかな?」
「P2Pネットワークっていっても、結局は、自分が待ち受けてるIPアドレスとポート番号を他のCoreノードと共有して、現在接続されているノードのメンバーに変更があったら、それを全体に周知する仕組みっていうだけだよね。だとしたら、やっぱり、そのリストが肝ってことになるんじゃないかなぁ」
「そのとおり。でも今の実装だと、たとえばEdgeノードがCoreノードのリストを取得した後で、どこかのCoreノードに対してまったく偽の情報を含むCoreノードリストを送りつけたら、どうなると思う?」
「……。あっ!」
「わかったかい?」
「なんのチェックもないので、送り付けられてきたCoreノードのリストで、自分がいま持ってるCoreノードのリストを上書きしちゃう!」
「そうだ。このままではノーガード過ぎてまずい。特定のサーバーをP2Pネットワークから存在していないものとして切り離すことが簡単にできてしまう」
「それはまずいね。どうすればいいだろう?」
「正規のCoreノードリストに含まれているメンバー以外からはリストを受け取らないようにすればいい。要は、そのための仕組みが実装から根本的に抜け落ちているところに問題がある」
「そっかー。Coreノードリストに含まれていないメンバーからCoreノードリストが届いても、それを信じて保存しちゃダメだよね……」
「ただし、それを素直に実装すると困ったことになるよ?」
「えっ、どういうこと?」
「他のCoreノードへ接続する処理のことを考えてみよう。最初に自分が持ってるCoreノードのリストは、どんなメンバーがいるかな?」
「あっ、そうか!最初の最初は自分しかいないから、正しいCoreノードに接続してリストをもらっても、それさえ捨ててしまうことになっちゃうな」
「その点を踏まえると、どういうふうに修正すべきだろう?」
「初回の接続時には自分が始原のCoreノードとして指定したものを信頼するしかないよね。そのうえで、リストの中に自分以外のCoreノードの情報が格納されるようになったら、その時点ではじめてリスト内のノードだけを信頼する形に切り替えればいいと思う」
「それじゃあ、そのようにコードを直してみようか」
<略> if cmd == MSG_CORE_LIST: if self.core_node_set.get_length() > 1: is_core = self.__is_in_core_set((addr[0], peer_port)) if is_core: new_core_set = pickle.loads(payload.encode('utf8')) alive_node_num = 0 # 厳密にはCoreノードであることを確認できていないが... for c_node in new_core_set: if c_node != (self.host, self.port): is_alive = self.__is_alive(c_node) if is_alive: alive_node_num += 1 if alive_node_num == len(new_core_set) - 1: print('Refresh the core node list...') print('latest core node list: ', new_core_set) self.core_node_set.overwrite(new_core_set) else: print('received unsafe core node list...from', (addr[0], peer_port)) else: print('MSG_CORE_LIST from Unknown node', (addr[0], peer_port)) else: if self.my_c_host == addr[0] and self.my_c_port == peer_port: new_core_set = pickle.loads(payload.encode('utf8')) print('List from Central. Refresh the core node list...') print('latest core node list: ', new_core_set) self.core_node_set.overwrite(new_core_set) else: print('received unsafe core node list... from', (addr[0], peer_port)) <以下略>
「正規のノードとしてCoreノードリストに格納されているっていうだけで無条件に信頼するのもどうかと思ったので、一応、本当にそのリストがP2Pネットワーク上のノードとして有効かどうかを接続して確認するようにしてみたよ」
「ふむ、よさそうだね」
「最初は、ガチのセキュリティを考えて、__is_alive
で接続する際には認証用の乱数なども送るような別の関数を作ろうかと思ったんだ。後でP2Pネットワークからの離脱みたいな処理の際に、その乱数入りの署名付きメッセージによる要求でなければ有効なものとして扱わない、みたいな……」
「なるほど。IPアドレス偽装のような問題まで考慮すると、万全なセキュリティを実現するためのプロトコルを考えるだけでも大変だぞ」
「そうなんだ。リストに署名を付けて暗号化して……、といった処理を入れたとしても、悪いことをしたのが誰かが後でわかるというだけで、リストを破壊しようという攻撃そのものが不可能になるわけじゃないし。だから、今のところ将来の改善点として念頭においておくだけにするけど、関数だけは分けておくことにしたんだ」
「そういう課題があることに気付いただけでも十分だよ。今回は関数を分けるだけにとどめておこう」
「できるだけオープンに参加してほしいっていう要件を満たしつつP2Pネットワークのセキュリティを考えるのって本当に難しいね……」
「では2つめのポイントに移ろうか」
「P2Pネットワークの課題は洗い出したよね。解決できていない部分はあるけど……」
「ああ。次のポイントというのは、ブロックチェーンのレイヤーで注意すべき問題だ」
「ブロックチェーンのレイヤーでもインターネットを意識しないといけないの?」
「厳密には、インターネットに公開することで、今まで作ってきたプログラム以外が接続してくる可能性があるという点に関連する注意事項だね」
「ああ、なるほど。PoWの処理をCとかで書いてもっと速く計算することでCoinbase Transactionをゲットしよう!みたいなことを考える人が出てくるかもなー、みたいなことは考えてた」
「まさにそういうケースの話だよ」
「でも、それってセキュリティの話なの?」
「攻撃ではないかもしれないけれど、プログラムが止まる、うまく動作しない、といった可能性があるなら、やはり問題にはなるだろう」
「そんなことが起こるんだ」
「SimpleBitcoinのロジックにおいて、PoWは、数字をどんどんカウントアップさせながらdifficulty
で指定した値と同じ桁数で0が並ぶハッシュ値が得られるまで処理を繰り返す、というものだったよね」
「うん。difficulty
は固定値だけどね」
「今はまだ実験的に小さな値でdifficulty
を固定しているけど、実運用でマシンパワーに合わせた大きな値にした場合、いつまでたっても延々と解が得られずにカウントアップが続く可能性があるんじゃないかな?」
「たしかに、永遠かどうかはともかく、膨大な数の再計算が必要になる可能性はあるね」
「そう。そして現状のSimpleBitcoinはPythonで記述しているので、カウントアップが延々と続いて桁溢れとか整数オーバーフローなどの事象が起きたとしても、何もなかったかのように動く。しかし、すべてのプログラミング言語がそうというわけではない」
「あー、そうなのか……」
「他のプログラミング言語で書かれたサーバーとの接続性まで考慮すると、PoWの処理はどう考えるべきか、他の暗号通貨のことを調べてみるのもいいかもしれないね」
「なるほどなー」
「次は、SimpleBitcoinでは重視していないセキュリティについて見ていくことにしよう」
「匿名性の話だっけ?」
「そうだ。トレーサビリティという言い方をすることもある。要は、コインが誰から誰に移動したのかについて、どれくらい把握しやすくなってるかっていう話だ」
「名称に暗号って含まれているくらいだし、暗号通貨って本来はトレーサビリティが低いほうがいいんだよね?」
「一般的にはそう考えられているようだね。現金を想像してみても、自分が受け取ったお金の出どころを、受け取り元より以前にまで遡って調べたいと思う人は稀だろう」
「犯罪捜査だと、身代金の紙幣の番号を記録しておくといった話を聞くよね。でも、日常的に受け取るお金の出どころを心配しなきゃいけない世界は嫌だなぁ……」
「実は、SimpleBitcoinでも、一人で複数のWalletを起動して使い分けることで、自分の公開鍵情報を実際に知っている人が自分の取引情報をすべて確認できてしまうという状況は回避できる」
「たしかに、Walletに対応する公開鍵情報がわからなければ、送金相手が誰かは知りようがないものね」
「そのとおり。ビットコインなどでも、自分のコインの使途を簡単に確認できないようにしたい人たちは、同じようにアドレスを複数用意して利用している。ただし、一般的には、Walletは1つのアプリで完結させているが」
「その点、SimpleBitcoinでは、Walletアプリごとに1つのアドレスしかサポートできないね」
「もちろん、あくまで現状の実装がそうなっているだけなので、ビットコインなどと同様な形に進化させていくことも可能だ」
「でも、SimpleBitcoinとしてはそうしないんだよね?」
「ああ。むしろ、『いいね!を贈りあってランキングを出そう』というSimpleBitcoinのコンセプトからすると、図6.1のように、送金した理由や、送金の際の補足メッセージのようなものがあったほうがいい。それによって送金相手を特定しやすくなっても、そのデメリットを受け入れる価値があると判断できる」
「SimpleBitcoinが目指す世界を実現するためには、自分のアドレスを秘密にする必然性がないってことだね」
「どのURLのTweetが褒められたとか、どこにアップロードした画像が褒められたとか、送金してもらうことによるコミュニケーションの要素を重視するのだから、公開鍵暗号をアドレスに使っていることは、『サインアップなしに会員登録できるようにするため』程度に考えるのがちょうどいいのかもしれない」
「コインの使途を他者から隠したい人は、多段階のWalletを経由することで匿名性のある使い方ができなくもないけど、そういうユーザーはSimpleBitcoinではどちらかというと特殊な人ってことだね」
「SimpleBitcoinのブロックチェーンにはすべての送金情報が記録されているから、図6.2に示すように、誰でもコインの流れを確認できる。それを理解した上で活用してほしい」
「匿名性よりもコミュニケーション重視に振った仕様であることが、SimpleBitcoinの特徴というわけだね」