非TLS時代のセキュアなデバイス認証の思い出

こんにちは。モバイルファクトリーには2011年に新卒で入社し、2016年まで働いていました。
これは非TLS時代のとあるゲームアプリでデバイス認証を実装した思い出の話です。もう使われていない仕組みだし、この機会に思い出を書いてみたいと思います。
いまとなっては役にたたない話が多いことをあらかじめ申し上げておきます。また、各暗号アルゴリズムに関する突っ込んだ話もここではしません。

# 背景

昨今はオープンなアクセスポイントの普及に伴って、そういったオープンなネットワークでも安全な通信を実現するためにTLSの普及が進んでいます。
ハードウェアそのものが高性能化したうえに、AWS CloudFrontやELBなどを利用してクラウドプラットフォーム上でTLS終端ができるようになったことで、TLSのためのハードウェアリソースやその負荷分散について考えなければならない場面はかなり減ってきたと思います。

当時のモバイルファクトリーではまだPerlbalがフロントエンドにいました。(※現在はPerlbalは動いておらずnginxやELB/ALBなどが使われているかと思います。)
[Perlbal](https://metacpan.org/pod/Perlbal)は名前の通りPerlで実装されたロードバランサーです。[Danga::Socket](https://metacpan.org/pod/Danga::Socket)を用いてイベントドリブンなアーキテクチャで実装されており、プラグイン機構を通じてPerlでロードバランサーの挙動をカスタムすることができました。
しかし、Perlには実用的に利用可能なスレッド機構がなく、Perlbalはシングルプロセス/シングルスレッドで動かすことが前提となります。Perlにも当然、OpenSSLのbinding libraryがあるので暗号化レイヤのパフォーマンスそのものはそこまで問題ではありませんでしたが、さすがにシングルスレッドではTLSを利用するとマシンリソースを使い潰してしまう懸念がありました。外向きのDNS RR(Round Robin)も実績がなく、何より当時はオンプレミスな運用だったのでGlobal IPを振れるサーバーも当然限られており、スケールアウトには限界がありました。

# ユーザー認証における課題

一方、アプリでユーザー認証をするときにも様々な問題がありました。ユーザーをどのようにIdenitifyするか、つまり何を以て同一のユーザーと断定するか、です。
当時からソーシャルゲーム業界はレッドオーシャンでした。厳しい競争に勝つためには他社より面白いアプリをUXを損ねずに提供しなければなりません。アプリストアのランキング上位に居るアプリはほとんどID/PASSWORDを登録させずに、インストールしてすぐに遊べるようになっていました。つまり表面的にはデバイス=ユーザーという構図になっており、(擬似的な)デバイス認証が行われていたのです。
デバイス認証は難しい側面が多く、ID/PASSWORD方式の認証が無難と呼ばれていた時代でしたが、デバイス認証によるアプリと戦っていくうえでそれでは現実的に難しかったためデバイス認証をどう実現させるか検討しました。では、どのようにデバイス認証が実現されていたのでしょうか。

## デバイス認証

たとえば、AndroidにおいてはIMEIを利用するのがデバイスの同一性を確認するための確実な手段のひとつでしたが、これはSIMから情報を取得するためプライバシー保護の観点で問題があり、それが故に警告を伴った権限要求ダイアログの表示とその承認を必要としました。端末のシリアルナンバーを利用するなどの他の手段もプライバシー保護の観点で問題がある点はそれほど変わりません。
そのため当時流行したのがUUIDを初回起動時に生成して端末に保存し、それをアプリ固有の擬似的な端末のIDとして利用する方法です。しかし、これにもいくつか課題があります。

まず、ゲームという特性上、攻撃者(チーター)がなりすましを狙うモチベーションが高いことに注意する必要があります。上位ユーザーになりすますことに成功すればそのユーザーの資産をすべて奪ったも同然となってしまいます。攻撃者に常識は通用しないので、たとえば端末ごと奪われてしまうケース、ネットワーク上のゲームの通信パケットが攻撃者に見えてしまうケースなども当然想定する必要があります。

その上での課題のひとつは、UUIDの保存方法です。保存したデータをそのまま使えない、置き換えられない[^1]ようにするための工夫が必要です。Android 4.3以降ではAndroid Keystore Systemを使うことができましたが、それ以前ではNDKのバイナリの中に鍵を埋め込み難読化したりするなどの泥臭い工夫をする必要がありました。（これに関しては別の機会があれば書きましょう）

ふたつめの課題は、UUIDの盗聴をどう防ぐかです。TLSを使ってペイロードを秘匿するのが確実でしたが、当時はPerlbalを使っていたので性能とスケールに懸念がありました。
そこで、行われた方法がTLSもどきを手実装してアプリケーションサーバーで解読させることです。アプリケーションサーバーであればPerlbalの裏側でスケールアウトさせることが可能だし、Preforkモデルのサーバーを利用していたのでCPUコアを有効に活用できます。

# TLSもどき

**なんども書きますがいまどきは普通にTLS使いましょうね？？？**

## ハンドシェイク

ハンドシェイクはこんな感じで出来ます。

0. あらかじめ、RSAの鍵ペア(X)を作りその公開鍵をクライアントに埋め込んでおく
0. クライアントはDH鍵交換パラメータを生成しサーバーに送信する
0. サーバーがDH鍵交換の共有鍵(Y)と端末を同定するためのID(端末ID)とRSA鍵ペア(Z)を生成する
0. サーバーが端末IDとZの秘密鍵を共有鍵Yで暗号化してDH鍵交換パラメータをセットにしたものをXで署名し、それをレスポンスとして返す
0. クライアントはDH鍵交換パラメータの署名をXの公開鍵で検証し、成功したらDH鍵交換の共有鍵Yを生成して署名キーZと端末IDを復号化する
0. 上記を保存する

当然、DH鍵交換に対する中間者攻撃に対してもXの署名を検証すれば攻撃を検知して中止できるし、暗号化した中身はまだ誰も使っていないデータなのでそれを復号化できたところで実際に使われることはなく無意味なデータとなり安全です。

ちなみに、[DH鍵交換の初期化ベクトルはJavaで簡単に任意ビット長のものを生成できる](https://techblog.karupas.org/entry/2015/01/14/174928
)のでその当時安全だと思われるビット長のものを選んで使いました。たしか。

これでクライアントは共有鍵Yと署名キーZと端末IDを手に入れました。

## リクエストの認証

クライアントからサーバーへの各リクエストには端末IDをYで暗号化したもの、nonceとなるランダム文字列、タイムスタンプ、そしてそれらをZで署名したものの4つをHTTPヘッダに付けてサーバーに送ります。
サーバーでは暗号化した端末IDから対応するZの証明書を得て、nonceやタイムスタンプとともに署名を検証することでなりすましでないことを確認できます。タイムスタンプの検証でリプレイ攻撃もある程度防ぐことができます。

着想から実装完了までだいたい2~3週間くらいだったと思うので今あらためて真面目に考えたらもう少し楽できた、もう少しセキュアにできた部分はありそうな気がします。（いまだったらTLS使えばいいから考えないが…）

# まとめ

TLSもどきをアプリケーションレイヤの上で再実装してアプリケーションサーバーで暗号化レイヤをスケールアウトできる仕組みを作った思い出の話でした。
モバイルファクトリーでは（あくまでも問題解決の手段としてですが）このようなアグレッシブな挑戦を何度もやることができて本当に勉強になりました。
機会があってやる気を見せれば挑戦させてくれる環境ってところはいまもきっと変わっていないと思うので、ぜひ今後もモバイルファクトリーは頑張って行ってほしいと思います。

明日は @koropicot さんです。楽しみ！

[^1]: 置き換えられてしまうとSession Fixation攻撃のように任意のIDでプレイさせる攻撃が可能になってしまいます。