秘密鍵の管理方法と悪用への対策

[以前書いた記事](https://qiita.com/cryptomikan/items/8230f1f383489e60bc97)でも触れていますが秘密鍵の管理は超重要事項で、これは NEM に限らず暗号通貨に共通して言えることです。
今回は、 NEM 特有の情報も含まれていると思いますが秘密鍵の管理をどうすれば良いか、仮に盗まれてしまった場合にどのように備えるべきか考えていきます。

なお、筆者はセキュリティエンジニアではありません。
つまりそちらは素人なので、気になるところはどんどん指摘していただきたいし、この記事の情報を元に不利益が出ても保証することはできません。ご注意ください。

<h2>秘密鍵を扱わなくて良い例</h2>

そもそも秘密鍵をアプリケーション内で扱う必要性が特にないならば、扱わないのが最強です。無いものは盗めません。
例えば、[SNEMS](http://www.snems.com/) や [PacNEM](https://www.pacnem.com/) のように特定のアドレスに対して入金があった場合にそれをアプリケーションに反映する場合、アドレス情報だけで入金履歴は取れるのでアプリケーション内で秘密鍵を扱う必要はありません。
他にもアドレスや公開鍵から取れる情報だけで目的を達成できるのであれば、アプリケーションに秘密鍵を含めるのはやめましょう。

<h2>他の秘密鍵を扱うプロダクトはどうしているのか</h2>

秘密鍵を絶対に扱うものの1つにフォーセットがあります(例：[NEM Mainnet Faucet](http://namuyan.dip.jp/nem/mainnet_nodb/) )。
送金を行う必要があるからです。

`crypto currency faucet source code` 等と検索すると、いくつかのフォーセットのソースコードが公開されています。しかし残念ながらコンフィグファイルに直書きされており、参考になりそうなものはありませんでした。公開されているものなのでこれは仕方ないのかもしれません。。。

あとは NEM の方で出しているウォレットアプリも同様にソースコードが公開されていましたが、これは後ほど触れます。

他にも事例があれば教えていただきたいです！

<h2>マルチシグ</h2>

マルチシグは万能ではありませんが、例え１つ秘密鍵が漏れてしまったとしてもそれ単体でできることは限られるため比較的安全になります。現実的にはクライアントからリクエストを送り、他の署名者がずっと NanoWallet 等を監視して手動で署名…なんてことはできないのでどこかのサーバーに秘密鍵を置いて、怪しいトランザクションでなければプログラムで自動的に署名するような運用になるでしょう。
基本的な実装や注意点等の話は[前の記事](https://qiita.com/cryptomikan/items/8230f1f383489e60bc97)を参照してください。

<h2>送金時の流れ</h2>

より詳細な話をする前に送金時の流れを見てみましょう。
![multisig1.png](https://qiita-image-store.s3.amazonaws.com/0/213908/b8c0cff0-d85e-ba46-cf98-0951ca8bbc30.png)

マルチシグでの送金は図のように以下のような流れで行われます。
①NIS (NEM Infrastructure Server) に送金リクエストを出す
②未承認のマルチシグトランザクションが発行される
③署名する（今回はVPS等から自動的に署名する想定）
④承認されたリクエストがブロックチェーンに含まれ、送金が確定する

このうち②と④は NEM のシステム上で実行され、ユーザーは関わることができません。逆に言うとこれらはハッキング等の心配は（現時点では）ない部分になります。

![multisig.png](https://qiita-image-store.s3.amazonaws.com/0/213908/aade3d0f-80b1-5ae0-76e3-b68ebf9500fe.png)


ここからはクライアント、①、③に対して予想される攻撃と対策を考えていきます。なお、基本的にどの問題も完璧な対策を施すことは不可能です。そのため、対策は破られる前提で妥協点を探る感じになっています。また、今作っているゲームに沿って考えたので網羅的では無いです。そのゲームではフォーセット機能をつけるつもりなので、送金に秘密鍵を使う予定です。

<h2>問題１. メモリに乗る秘密鍵はどうすれば良いか</h2>

例えば `string hoge = “秘密鍵”;` というものがあれば、C#の場合メンバ変数であればヒープに、ローカル変数であればスタックに `秘密鍵` が乗ります。そして、方法は載せて良いのか迷ったので割愛しますが、それぞれの場合に読み取ることはできてしまいました。
ヒープよりはスタックの方が安全ですので、第一歩としてローカル変数として定義/読み込みを行うようにすると良いです。

[NEMが出しているウォレット](https://github.com/NemProject/NEMiOSApp/blob/06b1400b67049c4dd5ba53445a4384388e1e3a47/NEMWallet/Application/Transaction/TransactionManager.swift#L182) はどうしているか見てみましたが、普段は暗号化しておき使用する瞬間だけ復号して使っているようでした。基本的にはそのように扱うのが良さそうです。あとは[メモリチートを防止するアセット](https://assetstore.unity.com/packages/tools/utilities/anti-cheat-toolkit-10395)を使うのも良いでしょう。ただし、その場合でも読み取り自体は難易度は上がりますが不可能ではないので秘密鍵が盗まれる可能性は考慮して対策を行いましょう。対策は問題２と同一のため後述します。

また、今回は秘密鍵を中心とした内容のため省きますが、メモリ改ざんによるデータの改ざん等にも注意する必要があるでしょう。
参考：[ゲームでよくされるチート手法とその対策 〜アプリケーションハッキング編〜](https://qiita.com/taptappun/items/b759df240c919d55c408)

こちらは非常に良い記事なので一読することをお勧めします。

<h2>問題２. 通信内容の改ざんは起こりうるか</h2>

4/18追記：[@44uk_i3](https://twitter.com/44uk_i3)さんが丁寧な補足記事を書いてくださいました。一読をおすすめします。
HTTPSの方が良い理由については一部ぼくの認識が違っていましたが、秘密鍵が漏れてしまった場合はやはり書き換えはできてしまうのかなという認識です。
http://blog.44uk.net/2018/04/16/nis-and-https-protocol/

NEM は NIS を経由して送金等を行なっているので、アプリケーションは必ず NIS とやりとりをすることになります。ただし現状 NIS はほぼ HTTPS に対応していません。つまり、前段階として秘密鍵がわかっていれば理論上は上書きができてしまうということのようです。
参考：[HTTPリクエストの改ざんをBurp Suiteでやってみた](http://www.stockdog.work/entry/2017/06/02/232327)

上書きできるかはリクエストの暗号化部分をちゃんと調べていないので断定はできないですが、可能性がある以上対策はやるべきと思うのでやります。
なお、念のため書いておきますがこれは NEM ネットワーク全体に重大な問題を起こせるとか、他の人の通信に割り込んで悪いことをできるという話ではなく、悪意あるユーザー A の手元で実行しているフォーセット付きゲームからのリクエストで A 宛に 10 XEM 送られるはずだったところを A が不正なリクエスト上書きを行って 114514 XEM 送らせるみたいなことができるという話です。他の全く関係ないウォレット等に悪さを働けるという話ではありません。そしてあくまで理論上は、です。ぼくにはできません。

この対策は3つ考えられます。１つは HTTPS 対応されている NIS を使用するか、自分で HTTPS 対応のノードを建てるか、クライアントやサーバーで対策するかです。 HTTPS 対応している ~~NIS は数が非常に少なく、~~ (4/16追記：現在は数が増えているようです。プラグインの設定に任せた場合は選ばれるか保証されないので、自分でリストを定義して使うようにすると良さそうですね。 
https://github.com/ethersecurity/nodes/blob/master/nem/nodes.txt) 自分で建てるのは難易度が高いため今回は比較的容易な自前で対策する方法を採ります。
対策と言っても簡単なもので、送金量や一定のルールに基づいて作成した文字列を暗号化し、マルチシグを行う別の VPS 上等で復号して問題なければ署名、という感じです。また、サーバー側を絡めた対策としてはアドレスごとのウォレットの金額はサーバーで握っておき、それ以上の金額がそのアドレスに対して送られようとした場合はブロック、等が考えられます。

<h2>問題３. クライアントのスクリプトが解析された場合</h2>

一番ヤバいです。完全に解析された場合、上記の対策はクライアント側で行っているものは意味が無くなります。
解析には静的解析、動的解析があります。動的解析をする人はなかなかいないようなので今回は静的解析について見ていきます。

現在ぼくが想定しているプラットフォームは WebGL です。つまり言語は JavaScript で、そのロジックはローカルに置かれます。ローカルにロジックに置かれると、どのプラットフォームでも頑張ればリバースエンジニアリングというのはできてしまうものです。そうなると秘密鍵は読まれるし、メッセージの暗号化ロジックもバレます。そして残念ながら、この攻撃に完全に対策することは不可能です。が、難読化を行うことである程度の対策にはなります。
参考：[ウェブアプリをソースごとパクる業者に対する対策](https://qiita.com/kacchan6@github/items/d8576ab6b3c16cf670ca)

Unity にも難読化を施すためのアセットは存在しています。ただ、Unity の WebGL はランタイムを asm.js で動作させるために C# を IL2CPP で C++ に変換した上で JS に変換、のようなエクストリームな手順を踏んでいて、この過程で結構難読化されています。
実際現時点ではリバースエンジニアリングツールは存在していないようです。勿論いずれ対応される可能性があるので難読化アセットを使うのも良いです。しかしその場合、当然ながらその場合実行速度は低下します。この辺りはトレードオフであるため作りたいアプリケーションを見ながらどうするか決めるべきでしょう。

なお、WebGL 以外の環境、特に Windows や Android でスクリプトバックエンドを Mono にしている場合はかなりの精度でコードを復元できるので注意しましょう。難読化アセットを使うか、最低でもバックエンドに IL2CPP を使いましょう。また、Unity ではなく JavaScript で普通に書いているような場合はノーガードに近い状態と思いますので、上記の記事等を参考に難読化を施しましょう。

先ほどのアドレスごとの残高をサーバーに記録しておく方法も有効ではあるのですが、
クライアント経由でサーバーに異常な値を記録されてしまった場合それすら信用できない可能性があります。
それらの守りも突破されたとして、最悪少量の XEM が送られるのは仕方ないとして、大量の XEM を送られるのは防ぎましょう。
この辺りはコンテンツの仕様によって変わると思われます。
例えばぼくの関わっているゲームの場合、

・普段フォーセットから得られる XEM は 1 以下
・フォーセットから得られるモザイクをチケットにゲームを遊べる
・ゲームでは格付けを行う。月間 １ 位には 100 XEM
・出金には最低 5 XEM 必要。出金時の額は選べず、全て出金される

のような仕様で考えています（額や仕様は未確定）
仕様を元におかしな行動を定義します。

・150 XEM 以上の送金
　→ 月間１位を取ったらその時点で出金するのでは？
・前回の出金から3日経過していないアドレスへの出金
　→ 月に一度しか大量に XEM を得られる機会はないはずなのに間隔が短すぎるのでは？

例えばこのような出金リクエストがあった場合、マルチシグの署名はサーバー側で自動的には行わないようにしておきます。そして管理者にメールを飛ばすなりして、管理者側での手動の署名を義務付けます。
この方法は属人的であり、管理者が一人しかいないのに入院等をしてしまい一時的に管理できないような場合等には大変困ったことになります。出金リクエストは全てサーバーで保存しておいてトランザクションの有効期限が切れたあとでも問題がなさそうであれば送金対応できるようにしておきましょう。

<h2>まとめ</h2>
- 置かなくて良い秘密鍵は置かない
- マルチシグを使う
- メモリハックに注意
- リクエストはHTTPSを使うなりして書き換えを防ぐ
- リバースエンジニアリングに注意
- 最悪サーバー側で止める

こんなところでしょうか。気になるところがある方はどんどんご指摘ください！