ネットワーク プロトコルを使用したセキュリティ

クライアント サーバーの暗号化された通信では、Transport Layer Security(TLS)を使用してアプリのデータを保護します。

この記事では、安全なネットワーク プロトコルのベスト プラクティスと公開鍵基盤(PKI)に関する考慮事項について説明します。詳細については、Android セキュリティの概要権限の概要をご覧ください。

概念

TLS 証明書を持つサーバーには、公開鍵とそれに対応する秘密鍵があります。サーバーは公開鍵暗号を使用して、TLS handshake 時に証明書に署名をします。

単純な handshake で証明できるのは、サーバーが証明書の秘密鍵を知っていることのみです。このような状況に対処するには、クライアントで複数の証明書を信頼できるようにします。クライアントサイドの信頼できる証明書セットに証明書が表示されない場合、そのサーバーは信頼できません。

ただし、サーバーは鍵のローテーションを使用して、証明書の公開鍵を新しい公開鍵に変更することがあります。サーバー設定が変更された場合、クライアント アプリのアップデートが必要です。サーバーがサードパーティのウェブサービス(ウェブブラウザ、メールアプリなど)の場合、クライアント アプリをアップデートするタイミングを判断するのはより難しくなります。

サーバーは通常、認証局(CA)証明書を使用して証明書を発行します。これにより、クライアントサイドの設定が徐々に安定します。CA は秘密鍵を使用してサーバー証明書に署名します。これにより、クライアントはプラットフォームにとって既知の CA 証明書がサーバーにあることを確認できます。

信頼できる CA は通常、ホスト プラットフォームにリストされます。Android 8.0(API レベル 26)には 100 を超える CA が含まれています。これは各バージョンで更新され、デバイス間で変更されることはありません。

CA はさまざまなサーバーの証明書を提供しているため、クライアント アプリはサーバーを確認するメカニズムが必要です。CA の証明書では、特定の名前(gmail.com など)またはワイルドカード(*.google.com など)を使用してサーバーを識別します。

ウェブサイトのサーバー証明書情報を表示するには、openssl ツールの s_client コマンドを使用して、ポート番号を渡します。デフォルトでは、HTTPS はポート 443 を使用しています。

このコマンドは openssl s_client の出力を openssl x509 に送信します。これにより、X.509 規格で証明書情報がフォーマット化されます。このコマンドはトピック(サーバー名)と発行元(CA)をリクエストします。

openssl s_client -connect WEBSITE-URL:443 | \
  openssl x509 -noout -subject -issuer

HTTPS の例

信頼できる CA により発行された証明書を取得済みのウェブサーバーがある場合、次のようなコードで安全にリクエストできます。

Kotlin

val url = URL("https://wikipedia.org")
val urlConnection: URLConnection = url.openConnection()
val inputStream: InputStream = urlConnection.getInputStream()
copyInputStreamToOutputStream(inputStream, System.out)

Java

URL url = new URL("https://wikipedia.org");
URLConnection urlConnection = url.openConnection();
InputStream in = urlConnection.getInputStream();
copyInputStreamToOutputStream(in, System.out);

HTTP リクエストをカスタマイズするには、HttpURLConnection にキャストします。Android の HttpURLConnection ドキュメントには、リクエスト ヘッダーとレスポンス ヘッダーの処理、コンテンツの公開、Cookie の管理、プロキシの使用、レスポンスのキャッシュなどの例が記載されています。Android フレームワークは、これらの API を使用して証明書とホスト名を検証します。

可能な限り、これらの API を使用してください。次のセクションでは、異なる解決策が必要とされる一般的な問題について説明します。

サーバー証明書の検証に関してよくある問題

getInputStream() がコンテンツを返す代わりに、例外をスローしたと仮定します。

javax.net.ssl.SSLHandshakeException: java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.
        at org.apache.harmony.xnet.provider.jsse.OpenSSLSocketImpl.startHandshake(OpenSSLSocketImpl.java:374)
        at libcore.net.http.HttpConnection.setupSecureSocket(HttpConnection.java:209)
        at libcore.net.http.HttpsURLConnectionImpl$HttpsEngine.makeSslConnection(HttpsURLConnectionImpl.java:478)
        at libcore.net.http.HttpsURLConnectionImpl$HttpsEngine.connect(HttpsURLConnectionImpl.java:433)
        at libcore.net.http.HttpEngine.sendSocketRequest(HttpEngine.java:290)
        at libcore.net.http.HttpEngine.sendRequest(HttpEngine.java:240)
        at libcore.net.http.HttpURLConnectionImpl.getResponse(HttpURLConnectionImpl.java:282)
        at libcore.net.http.HttpURLConnectionImpl.getInputStream(HttpURLConnectionImpl.java:177)
        at libcore.net.http.HttpsURLConnectionImpl.getInputStream(HttpsURLConnectionImpl.java:271)

この問題は、次のような場合に発生します。

  1. サーバー証明書を発行した CA が未知の CA だった場合
  2. サーバー証明書の署名が CA によるものではなく自己署名だった場合
  3. サーバー設定に中間 CA が存在しない場合

次のセクションでは、サーバーへの安全な接続を維持しながらこのような問題に対処する方法について説明します。

未知の認証局

SSLHandshakeException はシステムが CA を信頼していない場合に発生します。Android が信頼していない新規の CA が発行した証明書を保持している場合や、CA の指定がない古いバージョン上でアプリが実行されている場合に発生することがあります。CA が非公開であるため、ほとんど認識されていません。CA が未知になるケースとして多いのは、対象の CA がパブリック CA ではなく、政府、企業、教育機関などが内部で使用するために発行するプライベート CA だった場合です。

アプリのコードを変更せずにカスタム CA を信頼するには、ネットワーク セキュリティ構成を変更します。

注意: 多くのウェブサイトで、「何もチェックしない TrustManager をインストールする」という方法が説明されていますが、この方法は不適切です。この方法を採用すると、ユーザーが公衆 Wi-Fi アクセス ポイントを利用している場合に、攻撃を受けやすくなります。攻撃者は DNS を悪用し、デベロッパーのサーバーになりすましたプロキシを経由してユーザーのトラフィックを送信できるようになります。その結果、攻撃者はパスワードや各種個人データを記録できるようになります。攻撃者は証明書を生成することが可能で、信頼できる発行元からの証明書かどうかを検証する TrustManager が機能しないため、このような攻撃をブロックできません。したがって、たとえ一時的であっても、この方法は採用しないでください。代わりに、アプリがサーバー証明書の発行元を信頼するように設定します。

自己署名付きのサーバー証明書

次に、自己署名証明書により SSLHandshakeException が発生し、サーバーが独自の CA を保持することもあります。これは未知の認証局に類似した状況で、自己署名証明書を信頼するようにアプリのネットワーク セキュリティ構成を変更します。

中間認証局が存在しない

3 つ目は、中間 CA が存在しないために SSLHandshakeException が発生する場合です。パブリック CA はほとんどの場合、サーバー証明書に署名することはありません。代わりに、ルート CA が中間 CA に署名します。

不正使用のリスクを軽減するため、CA はルート CA をオフラインにします。ただし、Android などのオペレーティング システムでは通常、ルート CA のみ直接信頼されます。そのため、中間 CA が署名したサーバー証明書と、ルート CA 情報を持つ証明書検証クラスとの間で、信頼に関してわずかなギャップが生じることになります。

この信頼に関するギャップを解消するため、サーバーは TLS handshake 中に、サーバー CA から中間 CA を介して信頼できるルート CA に証明書チェーンを送信します。

例として、openssl s_client コマンドによって表示される mail.google.com 証明書チェーンを以下に示します。

$ openssl s_client -connect mail.google.com:443
---
Certificate chain
 0 s:/C=US/ST=California/L=Mountain View/O=Google LLC/CN=mail.google.com
   i:/C=ZA/O=Thawte Consulting (Pty) Ltd./CN=Thawte SGC CA
 1 s:/C=ZA/O=Thawte Consulting (Pty) Ltd./CN=Thawte SGC CA
   i:/C=US/O=VeriSign, Inc./OU=Class 3 Public Primary Certification Authority
---

上記の例の場合、サーバーは中間 CA である Thawte SGC CA が発行した mail.google.com 用の証明書と、Android が信頼するプライマリ CA である Verisign CA が発行した Thawte SGC CA 用の証明書の 2 つを送信しています。

ただし、必要な中間 CA を含めるようにサーバーが構成されていない場合もあります。Android ブラウザでのエラーと、Android アプリでの例外を引き起こす可能性があるサーバーの例は以下のとおりです。

$ openssl s_client -connect egov.uscis.gov:443
---
Certificate chain
 0 s:/C=US/ST=District Of Columbia/L=Washington/O=U.S. Department of Homeland Security/OU=United States Citizenship and Immigration Services/OU=Terms of use at www.verisign.com/rpa (c)05/CN=egov.uscis.gov
   i:/C=US/O=VeriSign, Inc./OU=VeriSign Trust Network/OU=Terms of use at https://www.verisign.com/rpa (c)10/CN=VeriSign Class 3 International Server CA - G3
---

未知の CA や自己署名のサーバー証明書とは異なり、ほとんどのパソコンのブラウザでは、このサーバーとの通信中にエラーは発生しません。パソコンのブラウザでは、信頼できる中間 CA をキャッシュしているためです。ブラウザがあるサイトから中間 CA に関する情報を得ると、次回からは証明書チェーン内にそれが存在していなくてもエラーにはなりません。

一部のサイトでは、リソース配信用のセカンダリ ウェブサーバーで、意図的にこの方法を採用している場合があります。帯域幅を節約するために、メイン HTML ページは完全な証明書チェーンを備えたサーバーから配信する一方で、画像、CSS、JavaScript などには CA を設定していないという場合があります。そのため、このようなサーバーが提供しているウェブサービスに Android アプリからアクセスしようとしても、許可されない場合があります。

この問題を解消するには、サーバー チェーンに中間 CA を含めるようにサーバーを設定します。ほとんどの CA では、一般的なウェブサーバー向けの設定手順が用意されています。

SSLSocket を直接使用する場合の注意事項

これまでに紹介した例では、HttpsURLConnection を使用する HTTPS に焦点を当ててきました。アプリによっては、HTTPS とは別に TLS を使用しなければならないことがあります。たとえば、メールアプリは、SMTP、POP3、IMAP 用の TLS を使用する可能性があります。このような場合、アプリは、HttpsURLConnection が内部的に実行するときとほぼ同じ方法で SSLSocket を直接使用できます。

これまでに説明した証明書検証に関する問題への対応方法が、SSLSocket にも当てはまります。実際、カスタムの TrustManager を使用するときに、HttpsURLConnection に渡されるのは SSLSocketFactory です。そのため、カスタムの TrustManagerSSLSocket を一緒に使用する必要がある場合は、手順はそのままで、SSLSocketFactory を使用して SSLSocket を作成します。

注意: SSLSocket はホスト名の検証は行いません。想定されるホスト名を使用して getDefaultHostnameVerifier() を呼び出すなど、アプリ側で独自にホスト名の検証を行う必要があります。また、エラーが発生した場合、HostnameVerifier.verify() は例外をスローしません。代わりにブール値の結果を返します。そのため、デベロッパー自身でその結果を明示的にチェックする必要があります。

ブロックされた CA

TLS はサーバーやドメインの確認済み所有者のみに証明書を発行するうえで、CA に依存しています。まれに、CA が悪用されたり、あるいは ComodoDigiNotar のケースのように CA がハッキングされたりすることがあり、その結果、ホスト名の証明書がサーバーやドメインの所有者以外に発行される場合があります。

このようなリスクを軽減するため、Android は特定の証明書や CA 全体を拒否リストに登録する機能を備えています。このリストは以前からオペレーティング システムに組み込まれていたものですが、Android 4.2 以降は、将来のセキュリティ侵害に対処できるようリモートで更新できるようになっています。

アプリを特定の証明書に限定する

注意: Android アプリでは、アプリで有効と見なされる証明書を以前に承認した証明書に制限する、証明書のピン留めは推奨されません。別の CA への変更など、将来サーバー構成が変更されたときに、証明書がピン留めされたアプリは、クライアント ソフトウェア アップデートを受信しないとサーバーに接続できなくなります。

アプリが指定した証明書のみを受け入れるように制限する場合は、複数のバックアップ用のピンを含めることが重要です。完全な管理権限を持つ 1 つ以上の鍵を、互換性の問題が発生しないように短い有効期限で含めるようにします。ネットワーク セキュリティ構成では、これを可能にする形でピン留めができます。

クライアント証明書

この記事では、TLS を使用してサーバーとの通信を保護する方法に焦点を当てます。TLS はサーバーがクライアントの ID を検証できるようにするクライアント証明書の概念もサポートしています。この記事では説明しませんが、その方法はカスタム TrustManager を指定する場合と同様です。

Nogotofail: ネットワーク トラフィック セキュリティのテストツール

Nogotofail は TLS / SSL の既知の脆弱性や設定ミスに対してアプリが安全であるかを簡単に確認できるツールです。このツールは強力かつ拡張可能な自動ツールであり、デバイスのネットワーク トラフィックを経由させることで、デバイス上のネットワーク セキュリティの問題をテストできます。

Nogotofail は主に次の 3 つのユースケースで役立ちます。

  • バグや脆弱性を見つける。
  • 修正を確認し、不具合を監視する。
  • トラフィックを生成しているアプリとデバイスを把握する。

Nogotofail は Android、iOS、Linux、Windows、ChromeOS、macOS に対応しており、インターネット接続に使用するほぼあらゆるデバイスで機能します。Android と Linux では、クライアントを使用して設定と通知の受信が可能です。攻撃エンジンそのものは、ルーター、VPN サーバー、プロキシとしてデプロイできます。

このツールは Nogotofail オープンソース プロジェクトから入手できます。

SSL と TLS のアップデート

Android 10

Google Chrome など一部のブラウザでは、TLS サーバーが TLS handshake の一環として証明書リクエスト メッセージを送信するときに、ユーザーが証明書を選択できます。Android 10 では、KeyChain.choosePrivateKeyAlias() を呼び出すときに KeyChain オブジェクトでは発行者と鍵の仕様パラメータを考慮して、ユーザーに証明書の選択を促すプロンプトが表示されます。このプロンプトにはサーバー仕様に準拠しない選択肢は含まれません。

サーバーの仕様に一致する証明書がない場合や、デバイスに証明書がインストールされていない場合など、ユーザーが選択できる証明書がない場合は、証明書の選択を促すプロンプトは表示されません。

さらに、Android 10 以降では、鍵または CA 証明書を KeyChain オブジェクトにインポートするために、デバイスが画面ロックされている必要はありません。

TLS 1.3 はデフォルトで有効

Android 10 以降、すべての TLS 接続に対して TLS 1.3 がデフォルトで有効になっています。TLS 1.3 実装に関する重要点は以下のとおりです。

  • TLS 1.3 暗号スイートはカスタマイズできません。TLS 1.3 が有効な場合、サポート対象の TLS 1.3 暗号スイートは常に有効です。setEnabledCipherSuites() を呼び出して無効にしようとしても無視されます。
  • TLS 1.3 がネゴシエートされると、セッションがセッション キャッシュに追加される前に、HandshakeCompletedListener オブジェクトが呼び出されます(TLS 1.2 以前のバージョンでは、このオブジェクトはセッションがセッション キャッシュに追加されてから呼び出されます)。
  • 以前のバージョンの Android では SSLEngine インスタンスにより SSLHandshakeException がスローされる状況において、Android 10 以降では代わりに SSLProtocolException がスローされます。
  • 0-RTT モードはサポートされていません。

必要に応じて、TLS 1.3 が無効になっている SSLContext を取得するには、SSLContext.getInstance("TLSv1.2") を呼び出します。また、対象オブジェクトに対して setEnabledProtocols() を呼び出すことで、接続ごとにプロトコル バージョンの有効と無効を切り替えることもできます。

SHA-1 で署名された証明書は TLS では信頼されない

Android 10 では、SHA-1 ハッシュ アルゴリズムを使用している証明書は、TLS 接続では信頼されません。2016 年以降、このような証明書はルート CA が発行していないため、Chrome などの主要ブラウザでは信頼されなくなりました。

SHA-1 を使用している証明書を提示するサイトへの接続はすべて失敗します。

KeyChain の動作変更と機能強化

Google Chrome など一部のブラウザでは、TLS サーバーが TLS handshake の一環として証明書リクエスト メッセージを送信するときに、ユーザーが証明書を選択できます。Android 10 では、KeyChain.choosePrivateKeyAlias() を呼び出してユーザーに証明書の選択を促すプロンプトを表示するときに、KeyChain オブジェクトによって発行者と鍵の仕様パラメータが考慮されます。このプロンプトにはサーバー仕様に準拠しない選択肢は含まれません。

サーバーの仕様に一致する証明書がない場合や、デバイスに証明書がインストールされていない場合など、ユーザーが選択できる証明書がない場合は、証明書の選択を促すプロンプトは表示されません。

さらに、Android 10 以降では、鍵または CA 証明書を KeyChain オブジェクトにインポートするために、デバイスが画面ロックされている必要はありません。

TLS と暗号化に関するその他の変更

Android 10 では、TLS と暗号化ライブラリに対する軽微な変更が反映されています。

  • AES/GCM/NoPadding および ChaCha20/Poly1305/NoPadding 暗号は、getOutputSize() からより正確なバッファサイズを返します。
  • TLS 1.2 以降の最大プロトコルを使用した接続試行では、TLS_FALLBACK_SCSV 暗号スイートが省略されます。TLS サーバーの実装が強化されているため、TLS 外部のフォールバックを試みることはおすすめしません。代わりに、TLS バージョン ネゴシエーションの使用をおすすめします。
  • ChaCha20-Poly1305 は、ChaCha20/Poly1305/NoPadding のエイリアスとして機能します。
  • 末尾にピリオドが付いたホスト名は、有効な SNI ホスト名とは見なされません。
  • 証明書応答用の署名鍵を選択するとき、CertificateRequest の supported_signature_algorithms 拡張機能が考慮されます。
  • Android Keystore の署名鍵など、不透明な署名鍵は TLS の RSA-PSS 署名で使用できます。

HTTPS 接続の変更

Android 10 で実行しているアプリが null を setSSLSocketFactory() に渡すと、IllegalArgumentException が発生します。以前のバージョンでは、setSSLSocketFactory() に null を渡した場合、現在のデフォルト ファクトリを渡す場合と同じ効果がありました。

Android 11

SSL ソケットはデフォルトで Conscrypt SSL エンジンを使用する

Android のデフォルトの SSLSocket 実装は、Conscrypt に基づいています。Android 11 以降、この実装は Conscrypt の SSLEngine の最上位に組み込まれます。