Skip to content

Most visited

Recently visited

navigation

HTTPS および SSL によるセキュリティ

技術的には Transport Layer Security(TLS)と呼ばれる Secure Sockets Layer(SSL)は、クライアントとサーバー間の暗号化通信の一般的な構成要素です。アプリで SSL を不適切な方法で使用すると、ネットワークを介して悪意のあるエンティティがアプリのデータを傍受できるようになる可能性があります。この記事では、アプリのデータが傍受されないようにするために、安全なネットワーク プロトコルを使用する際の一般的な落とし穴について説明するとともに、パブリック キーのインフラストラクチャ(PKI)の使用に関するいくつかの懸念事項を取り挙げています。

コンセプト

SSL の通常の使用シナリオでは、パブリック キーおよび一致するプライベート キーが含まれる証明書を使ってサーバーを設定します。SSL クライアントとサーバー間のハンドシェイクの一部として、サーバーは、パブリック キー暗号化で証明書に署名することによりプライベート キーがサーバーにあることを証明します。

ただし、誰でも独自の証明書とプライペート キーを生成することができるため、単純なハンドシェイクでは、プライベート キーが証明書のパブリック キーに一致することをサーバーが認識していること以外は、サーバーに関して何も証明することができません。この問題を解決するための 1 つの方法は、クライアントに信頼できる 1 つ以上の証明書のセットをインストールすることです。証明書がこのセットにない場合は、そのサーバーを信頼することはできません。

このシンプルなアプローチには、いくつかのデメリットがあります。サーバーでは、時間の経過に伴い、より強力なキーにアップグレード("キー ローテーション")し、証明書のパブリック キーを新しいキーに置き換える必要があります。残念ながら、現在はサーバーの設定を変更すると、クライアント アプリをアップデートする必要があります。これが特に問題となるのは、たとえば、サードパーティのウェブサービスの場合など、アプリ デベロッパーがサーバーを制御できないときです。また、ウェブブラウザやメールアプリなどのアプリが任意のサーバーと通信する必用がある場合、このアプローチでは問題が発生します。

これらのデメリットに対処するために、通常、サーバーは、証明機関(CA)と呼ばれるよく知られた発行元からの証明書を使って設定されます。一般的に、ホストのプラットフォームには、よく知られた信頼できる CA のリストが含まれています。Android 4.2(Jelly Bean)以降の Android には、現在、各リリースでアップデートされた 100 以上の CA が含まれています。サーバーの場合と同じように、CA には証明書とプライベート キーがあります。サーバーの証明書を発行するときに、CA はそのプライベート キーを使用してサーバー証明書に署名します。その後、クライアントは、プラットフォームのリストにある CA によって発行された証明書がサーバーにあることを確認できます。

ただし、CA の使用で解決される問題がある一方で、別の問題も発生します。CA は多くのサーバーの証明書を発行するため、目的のサーバーと通信していることを確認する方法が必要になります。この課題に対処するために、CA が発行した証明書では、gmail.com などの特定の名称または *.google.com などのホストのワイルドカード セットでサーバーを識別します。

次の例では、これらのコンセプトをより具体的に示しています。コマンドラインの以下のスニペットでは、openssl ツールの s_client コマンドが Wikipedia のサーバー証明書情報を参照しています。ポート 443 は HTTPS の既定値であるため、ポート 443 が指定されています。このコマンドにより openssl s_client の出力が openssl x509 に送信され、X.509 標準に応じて証明書に関する情報がフォーマットされます。このコマンドがサブジェクト(サーバー名の情報が含まれる)と発行元(CA を識別する)を照会していることに注意してください。

$ openssl s_client -connect wikipedia.org:443 | openssl x509 -noout -subject -issuer
subject= /serialNumber=sOrr2rKpMVP70Z6E9BT5reY008SJEdYv/C=US/O=*.wikipedia.org/OU=GT03314600/OU=See www.rapidssl.com/resources/cps (c)11/OU=Domain Control Validated - RapidSSL(R)/CN=*.wikipedia.org
issuer= /C=US/O=GeoTrust, Inc./CN=RapidSSL CA

RapidSSL CA により *.wikipedia.org に一致するサーバーの証明書が発行されていることが確認できます。

HTTPS の例

よく知られた CA が発行した証明書を備えたウェブサーバーでは、次のようなシンプルなコードを使って安全なリクエストを実行することができます。

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

実際にこのようにシンプルなコードが使用できます。HTTP リクエストを調整する場合は、HttpURLConnection にキャストすることができます。HttpURLConnection の Android ドキュメントには、リクエスト ヘッダーとレスポンス ヘッダー、コンテンツの投稿、Cookie の管理、プロキシの使用、レスポンスのキャッシュなどを扱う方法の例が記載されています。ただし、証明書とホスト名を確認するための詳細情報に関しては、Android フレームワークで 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 が不明である
  2. CA によってサーバー証明書が署名されていないが、自己署名されている
  3. サーバー設定に中間 CA がない

次のセクションでは、サーバーへの接続を安全に保ったまま、これらの問題に対応する方法について説明します。

不明の認証局

この場合、システムで CA が信頼されないため、SSLHandshakeException が発生します。Android で信頼されない新しい CA が発行した証明書を保持しているか、CA がない以前のバージョンでアプリを実行してる場合、この例外が発生する可能性があります。多くの場合、CA がパブリック CA ではないと、CA が不明になりますが、政府、企業、教育機関などでは、独自のプライベート CA から発行したプライベート証明書が使用される場合があります。

幸いにも、特定の CA のセットを信頼するように HttpsURLConnection に指示することができます。この手順はやや複雑なため、InputStream から特定の CA を取得し、その CA を使用して KeyStore を作成する例を以下に示します。その後、作成した KeyStore を使用して、TrustManager を作成して初期化します。システムは TrustManager を使用して、サーバーからの証明書を検証し、1 つまたは複数の CA を使って KeyStore から証明書を作成することにより、これらの CA のみが TrustManager によって信頼されるようになります。

この例では、新しい TrustManager がある場合に、HttpsURLConnection からデフォルトの SSLSocketFactory をオーバーライドするときに使用できる SSLSocketFactory を提供する新しい SSLContext を初期化しています。接続時は、証明書の検証のために CA がこのように使用されます。

この例では、University of Washington の組織 CA を使用しています。

// Load CAs from an InputStream
// (could be from a resource or ByteArrayInputStream or ...)
CertificateFactory cf = CertificateFactory.getInstance("X.509");
// From https://www.washington.edu/itconnect/security/ca/load-der.crt
InputStream caInput = new BufferedInputStream(new FileInputStream("load-der.crt"));
Certificate ca;
try {
    ca = cf.generateCertificate(caInput);
    System.out.println("ca=" + ((X509Certificate) ca).getSubjectDN());
} finally {
    caInput.close();
}

// Create a KeyStore containing our trusted CAs
String keyStoreType = KeyStore.getDefaultType();
KeyStore keyStore = KeyStore.getInstance(keyStoreType);
keyStore.load(null, null);
keyStore.setCertificateEntry("ca", ca);

// Create a TrustManager that trusts the CAs in our KeyStore
String tmfAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
tmf.init(keyStore);

// Create an SSLContext that uses our TrustManager
SSLContext context = SSLContext.getInstance("TLS");
context.init(null, tmf.getTrustManagers(), null);

// Tell the URLConnection to use a SocketFactory from our SSLContext
URL url = new URL("https://certs.cac.washington.edu/CAtest/");
HttpsURLConnection urlConnection =
    (HttpsURLConnection)url.openConnection();
urlConnection.setSSLSocketFactory(context.getSocketFactory());
InputStream in = urlConnection.getInputStream();
copyInputStreamToOutputStream(in, System.out);

CA を認識しているカスタム TrustManager を使用すると、サーバー証明書が信頼できる発行元から発行されたことを検証できます。

警告: 多くのウェブサイトでは、TrustManager をインストールするという不適切なソリューションについて記述されていますが、このソリューションは役に立ちません。このソリューションを実行した場合は、他人が DNS を悪用して、サーバーになりすましたプロキシを介してユーザーのトラフィックを送信することにより、パブリック Wi-Fi アクセス ポイントでユーザーを攻撃することができるようになるため、通信を暗号化していないことと同じになります。その後、攻撃者はパスワードやその他の個人データを記録することができます。攻撃者が証明書を生成できるので、その証明書が信頼できるソースから発行されたことを TrustManager で実際に検証することなしに、アプリでユーザーが他のユーザーと通信する可能性があるため、この攻撃手法は効果的です。したがって、一時的であっても、このソリューションを実行しないでください。アプリでは、いつでもサーバー証明書の発行元を検証できるため、検証を必ず実行してください。

自己署名サーバー証明書

SSLHandshakeException の 2 番目のケースは、自己署名証明書により発生します。つまり、サーバーが自身の CA として動作する場合です。これは、不明の認証局に類似しているため、前のセクションと同じアプローチを使用できます。

独自の TrustManager を作成することができます。今回は、サーバー証明書を直接信頼します。このアプローチには、前に説明したすべてのデメリット(アプリを証明書に直接関連付ける場合)がありますが、このアプローチは安全に実行することができます。ただし、自己署名証明書には、ある程度強力なキーが必要であることに注意してください。2012 年以降、毎年期限が切れる指数 65537 を備えた 2048 ビット RSA 署名が使用できるようになっています。キーのローテーションを行うときは、使用できるキーに関して、権威のある機関(NIST など)の推奨事項を確認する必要があります。

中間認証局が存在しない

SSLHandshakeException の 3 番目のケースは、中間認証局が存在しないために発生します。ほとんどのパブリック CA は、サーバー証明書を直接署名しません。その代わり、パブリック CA は、ルート CA と呼ばれるメイン CA 証明書を使用して、中間 CA に署名します。このようにすると、ルート CA をオフラインで保存して、侵害のリスクを軽減することができます。ただし、通常、Android などのオペレーティング システムでは、ルート CA のみが直接信頼されるため、中間 CA が署名したサーバー証明書と、ルート CA を認識している証明書検証クラスとの間にわずかなギャップが発生します。この問題を解決するために、サーバーは SSL ハンドシェイク中にサーバー証明書のみをクライアントに送信するのではなく、信頼できるルート 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 Inc/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 が長期に渡ってキャッシュされるからです。ブラウザが 1 つのサイトから中間 CA にアクセスし、中間 CA について学習すると、次回からは、証明書チェーンに中間 CA を含める必要がなくなります。

一部のサイトでは、リソースの提供に使用するセカンダリ ウェブサーバーに対して、この操作を意図的に実行します。たとえば、帯域幅を節約する目的などで、完全な証明書チェーンを備えたサーバーによってメイン HTML ページを提供し、画像、CSS、JavaScript などのリソース用のサーバーには CA を含めないようにする場合があります。残念ながら、Android アプリから呼び出そうとしているウェブサービスがこれらのサーバーによって提供されている場合がありますが、これは許容できません。

この問題を解決するには、次の 2 つのアプローチがあります。

ホスト名の確認に関する一般的な問題

この記事の最初に触れたように、SSL 接続を確認する場合は、2 つの重要な部分があります。最初の部分では、信頼できるソースから証明書が発行されたことを確認します。これは、前のセクションで説明しました。このセクションでは 2 番目の部分に焦点を合わせています。ここでは、通信先のサーバーが適切な証明書を提示することを確認します。サーバーが適切な証明書を提示しない場合、通常、次のようなエラーが表示されます。

java.io.IOException: Hostname 'example.com' was not verified
        at libcore.net.http.HttpConnection.verifySecureSocketHostname(HttpConnection.java:223)
        at libcore.net.http.HttpsURLConnectionImpl$HttpsEngine.connect(HttpsURLConnectionImpl.java:446)
        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 つの理由は、サーバー設定のエラーです。アクセスしようとしているサーバーに一致するサブジェクト フィールドまたはサブジェクト代替名フィールドがない証明書でサーバーが設定されています。多くの異なるサーバーで 1 つの証明書が使用されている可能性があります。たとえば、openssl s_client -connect google.com:443 | openssl x509 -text で google.com 証明書を確認すると、*.google.com をサポートするサブジェクトだけでなく、*.youtube.com や *.android.com などのサブジェクト代替名があることがわかります。接続しようとしているサーバー名が、証明書によって許可されるサーバー名としてリストされていない場合にのみ、このエラーが発生します。

残念ながら、別の理由(仮想ホスト)でこのエラーが発生することもあります。HTTP を使って、複数のホスト名で 1 つのサーバーを共有している場合、ウェブサーバーは、クライアントが探しているターゲット ホスト名を HTTP/1.1 リクエストから識別します。残念ながら、HTTPS では、この識別が複雑になります。サーバーが HTTP リクエストを認識する前に、どの証明書を返すかを識別する必要があるからです。この問題に対応するために、新しいバージョンの SSL(特に、TLSv.1.0 以降)では、Server Name Indication(SNI)がサポートされており、SSL クライアントでサーバーに対して目的のホスト名を指定することにより、適切な証明書が返されるようにすることができます。

幸いにも、Android 2.3 以降では、HttpsURLConnection が SNI をサポートします。Android 2.2(および以前のバージョン)をサポートする必要がある場合、この問題を回避する 1 つ方法は、一意のポート上に代替仮想ホストをセットアップし、どのサーバー証明書を返すかを明確にすることです。

より抜本的な回避方法は、HostnameVerifier を、仮想ホストのホスト名ではなく、デフォルトでサーバーが返すホスト名を使用する検証クラスに置き換えます。

警告: 他の仮想ホストを制御することができない場合、man-in-the-middle 攻撃により、知らないうちにトラフィックが別のサーバーに転送される可能性があるため、HostnameVerifier を置き換えることは非常に危険です。

ホスト名の確認をオーバーライドする必要がある場合のために、単一の URLConnection の検証クラスを、ホスト名が少なくともアプリで予測されるホスト名であることを引き続き確認する検証クラスに置き換える例を次に示します。

// Create an HostnameVerifier that hardwires the expected hostname.
// Note that is different than the URL's hostname:
// example.com versus example.org
HostnameVerifier hostnameVerifier = new HostnameVerifier() {
    @Override
    public boolean verify(String hostname, SSLSession session) {
        HostnameVerifier hv =
            HttpsURLConnection.getDefaultHostnameVerifier();
        return hv.verify("example.com", session);
    }
};

// Tell the URLConnection to use our HostnameVerifier
URL url = new URL("https://example.org/");
HttpsURLConnection urlConnection =
    (HttpsURLConnection)url.openConnection();
urlConnection.setHostnameVerifier(hostnameVerifier);
InputStream in = urlConnection.getInputStream();
copyInputStreamToOutputStream(in, System.out);

ただし、ホスト名の確認を置き換えるとき、他の仮想ホストを制御できない場合は、この問題を回避できる代替ホストの配置を見つける必要があるため、仮想ホストが原因で非常に危険な状態になることに注意してください。

SSLSocket を直接使用することに関する警告

これまでの例では、HttpsURLConnection を使用した HTTPS について重点を置いて説明しました。アプリで HTTP とは別に SSL を使用する必要がある場合があります。たとえば、メールアプリが SMTP、POP3、または IMAP の SSL バリアントを使用することがあります。こうした場合、HttpsURLConnection が内部で実行するときと同じような方法で、アプリは SSLSocket を直接使用します。

証明書の検証問題に対処するために説明した上記のセクションの手法が SSLSocket にも適用されます。実際、カスタム TrustManager を使用すると、SSLSocketFactoryHttpsURLConnection に渡されます。したがって、カスタム TrustManagerSSLSocket を使用する場合、同じ手順に従い、SSLSocketFactory を使用して、SSLSocket を作成します。

警告: SSLSocket はホスト名の検証を実行しません。予測されるホスト名を使用して getDefaultHostnameVerifier() を呼び出すことにより、アプリでホスト名の独自の検証を実行するようにしてください。また、エラーが発生した場合、HostnameVerifier.verify() は例外をスローせずに、明示的に確認する必要のあるブール型の結果を返すことに注意してください。

次の例は、この検証を行う方法を示しています。SNI のサポートなしに、gmail.com のポート 443 に接続すると、mail.google.com の証明書を受け取ることが示されています。この場合、これは予測された動作であり、実際に証明書が mail.google.com のものであることを確認する必要があります。

// Open SSLSocket directly to gmail.com
SocketFactory sf = SSLSocketFactory.getDefault();
SSLSocket socket = (SSLSocket) sf.createSocket("gmail.com", 443);
HostnameVerifier hv = HttpsURLConnection.getDefaultHostnameVerifier();
SSLSession s = socket.getSession();

// Verify that the certicate hostname is for mail.google.com
// This is due to lack of SNI support in the current SSLSocket.
if (!hv.verify("mail.google.com", s)) {
    throw new SSLHandshakeException("Expected mail.google.com, "
                                    "found " + s.getPeerPrincipal());
}

// At this point SSLSocket performed certificate verificaiton and
// we have performed hostname verification, so it is safe to proceed.

// ... use socket ...
socket.close();

ブラックリスト

SSL では、サーバーおよびドメインの適切に確認されたオーナーのみに証明書を発行するために CA を活用する必要があります。まれに、CA が悪用され、または ComodoDigiNotar の場合は CA が侵害されて、ホスト名の証明書が、サーバーやドメインのオーナー以外の誰かに発行されることがあります。

このリスクを軽減するために、Android は、特定の証明書または CA 全部をブラックリストに追加する機能を備えています。このリストはオペレーティング システムに継続して組み込まれており、 Android 4.2 以降では、このリストをリモートからアップデートして、今後の侵害に対処することができます。

ピン留め

ピン留めと呼ばれる手法を用いて、不正に発行された証明書からアプリを保護することができます。この手法では、基本的に、上記の不明な CA のケースで説明した例を使用して、アプリの信頼できる CA を、アプリのサーバーが使用できると認識しているより少数の CA のセットに制限します。これにより、システムの 100 以上の CA のいずれかが侵害されて、アプリの安全なチャンネルに危害が及ぶことを防止します。

クライアント証明書

この記事は、サーバーとの通信を保護するために SSL を使用するユーザーを対象にしています。SSL では、サーバーでクライアントの ID を検証することが可能なクライアント証明書の概念もサポートされます。この記事では説明しませんが、活用する手法は、カスタム TrustManager の指定と類似しています。HttpsURLConnection のドキュメントにあるカスタム KeyManager の作成に関する説明を参照してください。

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

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

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

Nogotofail は、Android、iOS、Linux, Windows, Chrome OS, OSX に対応しており、実際、インターネットへの接続に使用するあらゆる端末で機能します。Android や Linux に加えて、ルーター、VPN サーバー、またはプロキシとしてデプロイされる可能性のある攻撃エンジン自体に設定を構成して、通知を取得できる使いやすいクライアントがあります。

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

This site uses cookies to store your preferences for site-specific language and display options.

Get the latest Android developer news and tips that will help you find success on Google Play.

* Required Fields

Hooray!

WeChat で Google Developers をフォローする

Browse this site in ?

You requested a page in , but your language preference for this site is .

Would you like to change your language preference and browse this site in ? If you want to change your language preference later, use the language menu at the bottom of each page.

This class requires API level or higher

This doc is hidden because your selected API level for the documentation is . You can change the documentation API level with the selector above the left navigation.

For more information about specifying the API level your app requires, read Supporting Different Platform Versions.

Take a short survey?
Help us improve the Android developer experience. (Dec 2017 Android Platform & Tools Survey)