背景
前回の記事で、エンドユーザーの端末に秘密鍵を保持することなく、署名付きJWTを利用したクライアント認証を導入しようと頑張ってみたけど、結局、ユーザーの端末に偽のクライアントがインストールされてしまった場合には対応できないことが分かった。
ということで、今回はクライアント端末上のElectronアプリと中継サーバーの間で、正しいクライアントかどうかを検証することが出来ないものか考えてみた。
※(注意)自学習を目的として書いています。この記事に記載の内容が最適なやり方とは限りません。
偽のクライアントへの対応
前回までの内容を前提に、パブリックなネイティブアプリで、偽クライアントに対応する方法を検討してみる。
攻撃について
対応したいと考えている攻撃は、以下のような感じの攻撃。
- 悪意のあるユーザーが、正規のクライアントアプリを入手後、それを解析して(必要であれば、自分のアカウントを使って)、クライアントIDやリクエスト先のURLを記録し、それを使って、偽のクライアントアプリを作成する。
- 何らかの方法で、被害者のPCに偽のクライアントアプリをインストールさせる。
- 被害者は偽のクライアントアプリと知らずに、正規のエンドポイントで認証して認可コードを取得し、さらにアクセストークンまで取得する。
- 偽のクライアントアプリは、被害者のアクセストークンを攻撃者に送る。もしくは、アクセストークンで被害者に取得させた被害者の秘匿情報を攻撃者に送る。
対策について
思いついた対策案を書いてみる。
一応これも、前回試しに作ってみた構成が前提になっている。
- アプリのインストーラーはログイン済みのwebアプリ経由でのダウンロードとし、ダウンロード時に各インストーラーごとに一意なID(以降、クライアントアプリIDとする)をサーバー側で発行し、ログイン中のユーザーのIDを紐づけて、中継サーバー側に保存する。
- クライアントアプリのインストール時、クライアント端末にクライアントアプリIDを保存する。
- クライアントアプリは、トークンリクエスト時に、保存したクライアントアプリIDを一緒に中継サーバーに送る。
- 中継サーバーは、トークンエンドポイントから取得されたトークンの中の「sub」が、クライアントアプリIDに紐づくユーザーIDと一致しているかをチェックし、一致していなければ、トークンをクライアントアプリに返さない。
※補足:アプリをダウンロードしたユーザーしか使えないことになるが、webクライアントで利用ユーザー追加みたいな機能を用意すれば、たぶん対応できる。
対策の説明
攻撃者は攻撃者の端末にインストールした正規クライアントアプリのクライアントアプリIDは、コードを解読すれば取得可能なので、偽のアプリを使って、そのクライアントアプリIDを被害者に使わせることは可能と考えられる。
ただ、偽のクライアントアプリからのトークンリクエストで、攻撃者のクライアントアプリIDが中継サーバーに送られてきたら、中継サーバー側はクライアントアプリIDに紐づく「sub」の値が違っていることに気づける(もちろんクライアントアプリIDが送られてこなければリクエストを受け付けないようにする)。
しかしながら、攻撃者が被害者のクライアントアプリIDを入手出来たら破綻してしまうので、前提として、被害者の端末にインストールした偽のクライアントアプリなどから、被害者のクライアントアプリIDを入手するのが困難である必要がある。
課題
どうやってクライアントアプリIDを安全にクライアントの端末に保存するかが一番の課題。
keytarを使うと、同じ端末を利用する他のユーザーからは保護されるけど、偽アプリがサービス名を知っている前提であれば、被害者ユーザー自身がインストールした偽アプリからは保護できないことが分かった(少なくともwindowsでは)。
実際に別のアプリID、別のアプリ名を指定したwindows用electronアプリのインストーラーを2つ作成してインストールしたところ、同じサービス名、アカウントを指定すれば、相互にkeytarに保存した値を読み書き出来てしまった。
今のところ、思いつく対策案としては以下のような感じ。
インストーラーダウンロード時に毎回ビルドを走らせ、都度インストーラーが生成されるようにして、コード内にクライアントアプリIDを埋め込んだうえで、コードを難読化するようにする。
こうすれば、直接アクセスできない端末のクライアントアプリIDを取得するのは難しく(めんどくさく)なりそうな気がする。
さいごに
まぁ、結局のところ、完璧に保護するのは難しく、漏れたときのリスクを鑑みて、どこまでやるかっていう話なのかな。
そもそも、「OIDCとかの仕様で対応できるから、こんなことしなくても大丈夫」みたいな話なら良いんですが、調べてみたけど、このケースに対応できるものがあるのか分からなかった。。。
2021/01/12追記
OIDCの仕様を見ていて、「7. Self-Issued OpenID Provider」あたりが、今回のようなケースで利用できるんじゃないかと思った。今度調べてみようと思う。
https://openid.net/specs/openid-connect-core-1_0.html#SelfIssued