HTTPS と SSL を使用したセキュリティ

技術的には TLS(Transport Layer Security)と呼ばれる SSL(Secure Sockets Layer)は、クライアントとサーバー間の暗号化通信に欠かせない要素として広く普及しています。アプリが正しく SSL を使用していないと、悪意のある第三者がネットワーク上でアプリデータを傍受する可能性があります。この記事では、アプリのデータが傍受されないようにするために、セキュア ネットワーク プロトコルを使用する際に陥りやすい落とし穴や、公開鍵基盤(PKI)を使用する際の一般的な注意事項について説明します。

また、Android セキュリティの概要パーミッションの概要もご覧ください。

概念

SSL を使用する状況では通常、公開鍵を格納した証明書と、対応する秘密鍵を使用してサーバーを設定します。SSL クライアントとサーバー間のハンドシェイクの一部として、サーバーは、公開鍵暗号を使用して証明書に署名することで、サーバーが秘密鍵を保持していることを証明します。

ただし、独自の証明書と秘密鍵を生成することは誰でも可能です。そのため、単純なハンドシェイクでは、サーバーがその証明書の公開鍵と対になる秘密鍵を知っているということ以外、そのサーバーについては何も証明されません。この問題を解決する方法の 1 つは、クライアント サイドで、1 つまたは複数の証明書を「信頼できる証明書」のセットとして指定することです。対象の証明書がこのセットに含まれていない場合、サーバーは信頼されません。

ただし、この単純な方法にはいくつか弱点があります。サーバーは、時間の経過とともに強固な鍵にアップグレードして(「鍵のローテーション」)、証明書内の公開鍵を新しい公開鍵に置き換える必要があります。しかし、現在のところ、サーバーの設定を変更すると、通常はクライアント アプリのアップデートが必要になります。これが特に問題となるのは、サーバーがアプリ デベロッパーの管理下にない場合です(たとえば、サードパーティ ウェブサービスの場合)。また、ウェブブラウザやメールアプリなど、アプリが任意のサーバーと通信する必要がある場合にも、このアプローチでは問題が発生します。

このような弱点に対処するため、サーバーを設定する際は通常、CA(認証局)と呼ばれる信頼できる機関から発行された証明書を使用します。 一般的に、ホスト プラットフォームには、そのプラットフォームが信頼する周知の CA のリストが登録されています。 Android では、Android 4.2(Jelly Bean)以降、リリースごとに CA が更新され、現在は 100 を超える CA が登録されています。サーバーと同様に、CA も証明書と秘密鍵を保持します。サーバー用の証明書を発行する際、CA は CA 自身の秘密鍵を使用してサーバー証明書に署名します。これにより、クライアントは、サーバーの証明書が、プラットフォームの信頼する CA によって発行されたものであることを検証できます。

ただし、CA を使用することで解決できる問題がある一方、別の問題も発生します。CA は多数のサーバーの証明書を発行しているため、目的のサーバーと通信していることを確認するための手段がほかにも必要となるのです。この問題に対処するため、CA が発行した証明書は、具体的な名前(例: gmail.com)か、ワイルドカードを使用したホスト名(例: *.google.com)のいずれかを使用してサーバーを識別します。

この概念について、具体的な例を基に説明しましょう。以下のコマンドライン スニペットでは、openssl ツールの s_client コマンドが Wikipedia のサーバー証明書情報を検証しています。ポート 443 を指定しているのは、これが HTTPS のデフォルトであるためです。このコマンドは openssl s_client の出力を openssl x509 に送信します。これにより、証明書に関する情報が X.509 規格に沿って書式化されます。具体的に言うと、コマンドが要求しているのは、サーバー名情報を格納するサブジェクト(subject)と、CA を識別する発行元(issuer)です。

    $ 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
    

この例の場合、*.wikipedia.org に合致するサーバーに対して、RapidSSL CA が証明書を発行していることが確認できます。

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 にキャストできます。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 が未知の CA だった
  2. サーバー証明書の署名が CA によるものではなく自己署名だった
  3. サーバー設定に中間 CA が存在しない

セキュアなサーバー接続を維持しながらこの問題に対処する方法について、以下のセクションで説明します。

未知の認証局

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

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

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

この例では、ワシントン大学の組織 CA を使用しています。

Kotlin

    // Load CAs from an InputStream
    // (could be from a resource or ByteArrayInputStream or ...)
    val cf: CertificateFactory = CertificateFactory.getInstance("X.509")
    // From https://www.washington.edu/itconnect/security/ca/load-der.crt
    val caInput: InputStream = BufferedInputStream(FileInputStream("load-der.crt"))
    val ca: X509Certificate = caInput.use {
        cf.generateCertificate(it) as X509Certificate
    }
    System.out.println("ca=" + ca.subjectDN)

    // Create a KeyStore containing our trusted CAs
    val keyStoreType = KeyStore.getDefaultType()
    val keyStore = KeyStore.getInstance(keyStoreType).apply {
        load(null, null)
        setCertificateEntry("ca", ca)
    }

    // Create a TrustManager that trusts the CAs inputStream our KeyStore
    val tmfAlgorithm: String = TrustManagerFactory.getDefaultAlgorithm()
    val tmf: TrustManagerFactory = TrustManagerFactory.getInstance(tmfAlgorithm).apply {
        init(keyStore)
    }

    // Create an SSLContext that uses our TrustManager
    val context: SSLContext = SSLContext.getInstance("TLS").apply {
        init(null, tmf.trustManagers, null)
    }

    // Tell the URLConnection to use a SocketFactory from our SSLContext
    val url = URL("https://certs.cac.washington.edu/CAtest/")
    val urlConnection = url.openConnection() as HttpsURLConnection
    urlConnection.sslSocketFactory = context.socketFactory
    val inputStream: InputStream = urlConnection.inputStream
    copyInputStreamToOutputStream(inputStream, System.out)
    

Java

    // 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 として機能していることを意味します。「未知の CA」の場合と似ているため、前のセクションと同じ方法で対策できます。

このケースでは、サーバー証明書を直接信頼する独自の TrustManager を作成できます。この方法の場合、アプリを証明書に直接結び付ける場合(上記を参照)と同様の弱点がありますが、セキュアに行うことが可能です。ただし、自己署名証明書の鍵を十分に強固なものにする必要があります。2012 年以降、2048 ビット RSA 署名(指数 65537、期限 1 年)が許容されています。鍵をローテーションする際は、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 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 に関する情報を得ると、次回からは証明書チェーン内にその中間 CA が存在していなくてもエラーにはなりません。

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

この問題の解決策として、次の 2 つの方法があります。

  • サーバー チェーンに中間 CA を含めるようにサーバーを設定します。ほとんどの CA は、一般的なウェブサーバーでこの設定を行うためのドキュメントを用意しています。Android 4.2 以前のデフォルトの Android ブラウザでサイトが正常に機能できるようにする必要がある場合は、これが唯一の対処方法となります。
  • あるいは、中間 CA を他の未知の CA と同様に扱い、前の 2 つのセクションで行ったように、中間 CA を直接信頼する TrustManager を作成します。

ホスト名の検証に関してよくある問題

この記事の冒頭で説明したように、SSL 接続の検証では重要な要素が 2 つあります。1 つ目は、信頼できる発行元から証明書が発行されたのかどうか検証することであり、これについては、前のセクションで説明しました。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 つの証明書を複数のサーバーで共用している可能性があります。たとえば、google.com の証明書を openssl s_client -connect google.com:443 | openssl x509 -text で調べてみると、サブジェクトが *.google.com をサポートしている一方、サブジェクトの別名には *.youtube.com、*.android.com などが設定されていることがわかります。このエラーは、接続先のサーバーの名前が、接続可能なサーバーとして証明書に記載されていない場合に限り発生します。

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

Android 2.3 以降は、HttpsURLConnection が SNI をサポートしています。Android 2.2 以前をサポートする必要がある場合、対応策としては、一意のポート上に代替仮想ホストをセットアップし、どのサーバー証明書を返すのかを明確にする方法があります。

抜本的な対応策としては、HostnameVerifier を、仮想ホストのホスト名ではなく、サーバーがデフォルトで返すホスト名を使用する検証クラスに置き換えます。

注意: HostnameVerifier の置き換えは、他の仮想ホストがデベロッパーの管理下にないときは非常に危険な場合があります。中間者攻撃により、知らない間にトラフィックが別のサーバーに送信される恐れがあるためです。

ホスト名の検証をどうしてもオーバーライドする必要がある場合は、次の例をご覧ください。ここでは、単独の URLConnection 用の検証クラスを、少なくともアプリが期待するホスト名であるかどうかを検証する検証クラスに置き換えます。

Kotlin

    // Create an HostnameVerifier that hardwires the expected hostname.
    // Note that is different than the URL's hostname:
    // example.com versus example.org
    val hostnameVerifier = HostnameVerifier { _, session ->
        HttpsURLConnection.getDefaultHostnameVerifier().run {
            verify("example.com", session)
        }
    }

    // Tell the URLConnection to use our HostnameVerifier
    val url = URL("https://example.org/")
    val urlConnection = url.openConnection() as HttpsURLConnection
    urlConnection.hostnameVerifier = hostnameVerifier
    val inputStream: InputStream = urlConnection.inputStream
    copyInputStreamToOutputStream(inputStream, System.out)
    

Java

    // 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 を使用するときに、HttpsURLConnection に渡されるのは SSLSocketFactory です。そのため、カスタム TrustManagerSSLSocket を一緒に使用する必要がある場合は、手順はそのままで、SSLSocketFactory を使用して SSLSocket を作成します。

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

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

Kotlin

    // Open SSLSocket directly to gmail.com
    val socket: SSLSocket = SSLSocketFactory.getDefault().run {
        createSocket("gmail.com", 443) as SSLSocket
    }
    val session = socket.session

    // Verify that the certicate hostname is for mail.google.com
    // This is due to lack of SNI support in the current SSLSocket.
    HttpsURLConnection.getDefaultHostnameVerifier().run {
        if (!verify("mail.google.com", session)) {
            throw SSLHandshakeException("Expected mail.google.com, found ${session.peerPrincipal} ")
        }
    }

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

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

Java

    // 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 verification 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 の 1 つが不正使用されてアプリのセキュア チャネルがセキュリティ侵害を受けるという事態を回避できます。

クライアント証明書

この記事では、SSL のユーザーがサーバーと通信する際のセキュリティについて詳しく説明してきました。SSL は、サーバーがクライアントの身元を検証できるようにする「クライアント証明書」という概念もサポートしています。ここでは説明しませんが、手法は、カスタム TrustManager を指定する場合と同様です。詳細については、HttpsURLConnection のドキュメント内のカスタム KeyManager の作成方法をご覧ください。

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

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

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

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

Nogotofail は、Android、iOS、Linux、Windows、Chrome OS、OS X に対応しており、インターネット接続に使用するほぼあらゆるデバイスで機能します。各種プラットフォーム上で設定を行ったり通知を取得したりできる使いやすいクライアントが用意されています。Android や Linux だけでなく、ルーターや、VPN サーバー、プロキシとして導入される可能性のある攻撃エンジン上でも利用できます。

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