最近、仮想通貨のウォレットについてのシステムを実装する機会があったのでそこから得られた知見を書いてみます。
ライブラリは ビットコインのC#実装である NBitcoin を使いました。ただウォレットについては他の通貨も同様なので応用はききますし、C#以外の実装でも雰囲気はだいたい一緒でしょう。
さてビットコインに限らず、仮想通貨のウォレットとは公開鍵・秘密鍵のペアです。とりわけウォレットからの送金にあたっては、そのウォレットの持ち主だけが知っている秘密鍵で署名することで、確かに持ち主が送金許可しているとみなが確認できることがポイントです。
個人で使うならウォレットは1つでまあ用はたりる(現実世界でも財布は1人で同時に持ち歩くのはふつう1つですね)のですが、例えば仮想通貨取引所ではどうでしょうか? 仮想通貨取引所は、他のウォレットと仮想通貨の入金・出金のやりとりをするため、ウォレットが口座開設者それぞれに必要です。小規模な取引所で考えても、5種類の通貨で1万人の顧客の口座を扱えば、それだけで鍵ペアが5万個も必要になり、これを適切に管理するのは容易ではありません。適当なRDBに突っ込んだりしていてはコインチェックの二の舞です。現実世界で例えると、銀行の貸金庫で小さいボックスがずらりと並んでいるイメージでしょうか。
BIP32 HD Wallet
この問題を解決するのが**BIP32 HD Wallet** です。HDというのは hierarchical deterministic で、ある特定の情報(シードと呼びます)から出発して、鍵ペアを必要に応じていくらでも短時間で生成できるといういかした技術です。その後、これを拡張したBIP44というのも出ています。
これの雰囲気は、オンラインバンキング等でよくあるワンタイムパスワードに似ています。最初に同期をとっておけば、ワンタイムパスワード自体を盗み見られても後続の情報のセキュリティは失われない、というやつです。あるいは、昔のC言語からあるsrand()にも似ています。これは乱数を初期化しますが、同じシードからはいつも同じ乱数がrand()で得られる、というやつです。
これの要点としては以下になります。
- シードがあればいくらでも鍵ペアが高速生成できる
- 通貨の種類が違ってもウォレット生成ができる(同一ビット数の楕円曲線暗号を使う、という前提ですが)
- 鍵ペアを生成するパラメータをKeyPathと呼ぶ。同一シードと同一KeyPathからは常に同じ鍵ペアが得られるので、結果の鍵ペアを記憶する必要はない。
- 鍵ペアを見ただけでは、(それが秘密鍵であっても)シードの推定はできないし、複数の公開鍵をみて「同一シードから生成された」と判断を下すこともできない
KeyPath
KeyPathは以下の書式になるよう仕様がきめられています。(BIP32の場合。BIP44は若干複雑になってますが大差ありません)
m / purpose' / coin_type' / account' / change
purposeは44で固定されています。coin_typeは、Bitcoinは0、Ethereumは60、などと決められています。他にもたくさんの通貨があり、ここに登録 されています。
accountはユーザごとの値です。仮想通貨取引所で1万口座あれば、1~10000で割り振ればいいですね。口座ごとに違う値であれば何でもよろしい。数値が大きくても導出にかかる時間はほとんど(もしかすると全く)影響しません。
changeは用途を示すもので、0なら通常、1ならビットコインでの「おつり」用のアドレスということですが、まああまり厳密に区別しなくていいでしょう。僕もよくわかってません。
' がついているやつは hardened という意味で、これがない場合は1つ上のKeyPathでの鍵を遡って計算可能、ある場合は計算不可、ということらしいですがこれもちょっと調査が不十分です。
NBitcoinでは、
ExtKey root = new Mnemonic(Wordlist.English).DeriveExtKey();
ExtKey derived = root.Derive(new KeyPath("m/44'/0'/1'/0"));
のようにいけます。Mnemonicは後述しますが、シード値と同義です。ExtKeyは鍵ペアにあたるクラスです。
KeyPathクラスを使わずに、
ExtKey derived = root.Derive(44, ture).Derive(0, true).Derive(1, true).Derive(0, false);
としても同じ結果になります。2番目のbool型引数はhardenedかどうかを示すものです。
実は個人でも有用
ここまでの説明だと、BIP32は取引所ための仕様にみえますが、個人で使うにもそれなりに有用です。この仕組みで「n番目のアドレス」をいくらでも生成できるならば、
-
誰かからの送金があるとき →新規生成したn+1番のアドレスで受領する
-
誰かへ送金するとき →全財産の入っているn番のアドレスから、行いたい送金+残りの全財産をn+1番のアドレスへ送金する
という手続きを踏むことで、同一のアドレスはpublicな仮想通貨ネットワークに1回しか流さずに済むようになります。こうすることで、秘密鍵の特定はもちろん、同一人物の取引であることの追跡も極めて困難になるので情報の秘匿という点では非常に優れます。
BIP39 シードの管理
残る話題はシードの管理です。これがバレると、シードに紐づいたすべての鍵ペアがバレるということなのでこいつの管理はきわめて厳重にやらないといけません。インターネットから見える場所にシードを保管するのは論外ですが、適切に管理されたハードディスクなりSSDなりに記録するという時点ですでにかなりのリスクです。外部からの攻撃はもちろん、内部の人間が裏切ったり買収されたり、あるいは単にそのハードディスクが故障したり、という危険にさらされます。
個人をターゲットにそこまで執拗な攻撃をするとは考えられませんが、取引所をターゲットとするなら、コインチェック事件をみてわかるとおり成功報酬は数百億円レベルになるわけですから、本気で攻撃するなら準備や調査に1億円くらい使う価値は充分あります。守る側もその認識を持たないといけない。
となると安心できるのは**「機密情報をコンピュータ上に置かない」**というのが必要条件でして、そのための仕組みがBIP39です。これは、シード値を英単語の列(日本語を含む多言語のバージョンもあります)に変換するための仕様です。例えば
cheap travel twice valid jelly violin wild essence useless eight leisure dizzy bicycle window piece...
というようになります。使っていい単語リストはNBitcoinのソースを見るとわかります。
16進数の列よりはこの単語列のほうがまだ人間に優しいので、あとはこれを丸暗記するなり紙に印刷して金庫にしまうなり、ということになります。紙を持ち歩いて電車やタクシーに乗るのはダメですよ!
もっとも紙に記録しても内部犯行、武力による強奪、火災、大地震といった事象についてはリスクが残りますが、これはマルチシグという別の技術で回避できます。これも機会があれば書いてみます。
なおNBitcoinの場合、
string wl = "cheap travel twice valid jelly violin wild essence useless eight leisure dizzy bicycle window piece...";
Mnemonic m1 = new Mnemonic(wl, Wordlist.English);
とすれば文字列からシードの復元になりますし、
string[] wl = new Mnemonic(Wordlist.English).Words;
とすれば新規作成のシードから単語列を得ることができます。
最後に
ウォレットを作るだけならプログラマならすぐできると思いますし、どこかそのへんで買った通貨を自作ウォレットに送金し、そのトランザクションを公共のblockchain viewerで見るところまでなら簡単です。少額でやれば仮にミスっても損失は小さいのでぜひやってみましょう。自作ウォレットからの送金、となるともうちょっと話が複雑になるのでそれはまた別の稿で。