Android HTTPS正伝

8809 ワード

HTTPSがますます普及するにつれて、私たちはクライアントの開発過程でますますHTTPからHTTPSに変換するように要求されています.ブラウザではなく普通のアプリを開発していても、HTTPSネットワークコードを正しく書くことが重要です.しかし、現実的には、ほとんどのHTTPS関連コードには常に様々な問題が存在し、これらの問題により、私たちのAPPはネットワーク通信に深刻な脆弱性が存在し、HTTPSを使用する意味を失っている.
そこで、本稿では、HTTPSに対する理解と、HTTPSに対する理解の3つの面から紹介する.二つ目はよく見られるHTTPSの使用誤りである.3つ目は、HttpClientまたはHttpUrlConnectionを使用するなど、正しいHTTPS通信コードをどのように記述するかです.
HTTPSの理解
熟知している読者は直接スキップすることができて、簡単に言えばHTTPS=HTTP+SSL/TLSで、SSL/TLS=暗号化+認証+完全性保護で、HTTPSプロトコルは簡単に理解して伝統的なHTTP通信を行う前にまずSSL/TLSプロトコルの握手を通じて安全な通路を創立します.
実はSSL/TLSは暗号学の中の1つの混合暗号システムで、対称暗号化で機密性を保護して、メッセージ認証コード(MAC)で認証と完全性の保護をして、同時に対称暗号の鍵の配送の問題を解決するために、公開鍵の暗号化の方法を採用して、それでは公開鍵の合法性を保証するために、認証機構(CA)公開鍵と関連情報にデジタル署名を行います.つまり、私たちの口の中の証明書です.
実はHTTPSだけではなく、多くのアプリケーション層のネットワーク転送プロトコルがセキュリティを保証するためにSSL/TLSと結婚している.例えばFTPSでは、SSL/TLSのIMAP、POP 3/SMTP、Telnetプロトコルなどを採用している.
HTTPSの使用ミス
AndroidアプリがHTTPS上に存在する最大の問題は仲介者攻撃の抜け穴であり、仲介者攻撃とは、攻撃者が通信双方の間に介入し、互いに相手と通信するふりをしてプライバシー情報をカバーすることである.この脆弱性を招いた最大の原因は、コードの中でHTTPS証明書の不合理な使用であり、正確には、クライアントがサービス側のHTTPS証明書(署名CAが合法かどうか、ドメイン名が一致しているかどうか、自己署名証明書かどうか、証明書が期限切れになっているかどうかを検証していないことである..
次の2つのケースでよく見られます.
  • 1.カスタムX 509 TrustManagerはSSL証明書を検証していません.例えば、checkServer Trusted()メソッドが空に実装されている場合、サーバが信頼できるかどうかを確認しません.
  • 2.setHostnameVerifier(ALLOW_ALL_HOSTNAME_VERIFIER)を使用するか、カスタムHostnameVerifierを使用してverify()メソッドを使用してドメイン名を検証しないようにドメイン名を検証していません.

  • 開発者がこのように書く目的は、SSLの異常を回避することであることが多いが、HTTPSの能力を発揮していないだけでなく、かえって抜け穴が明らかになった.
    もちろん,仲介者の攻撃の原因には,CAが攻撃されて秘密鍵が盗まれたなど,他にもある可能性があるが,このことは本稿では論じない.
    HTTPSの正確な実現
    HTTPSをどのように正しく実現するかは、証明書をどのように正しく処理するかにかかっています.
    前述したように、証明書認証機関CAは、私たちのHTTPS通信で使用される公開鍵に署名して証明書を作成します.実は、サーバには公開鍵がセットされています.CAには公開鍵がセットされています.CAは、その秘密鍵で私たちの公開鍵にデジタル署名します.これが認証です.したがって、証明書の主な内容は3つの部分であり、a、サーバが生成した公開鍵、b、証明書を申請する際に記入した関連情報、c、CAは自分の秘密鍵でaとbの内容に対して生成したデジタル署名である.では、クライアントがサービス側の証明書を検証する場合、署名を検証するには、クライアントが証明書CAの公開鍵を持っていなければならない.
    したがって、クライアントがCAの公開鍵を持っているかどうかによって、証明書の処理は以下のような状況に分けられる.
    1.CAは信頼されている
    これはHTTPSの使用の中で最も推奨される方法である.すなわち、Symantec(VeriSignを買収した)のような有名な証明書発行機関を使用する.このような発行機関は一般的にブラウザやAndroidシステムに内蔵された信頼CAリストにある.これは、クライアントがCAの公開鍵を持っていることを意味し、HTTPS握手プロトコルに従ってサービス側から証明書を取得し、CAが認証した証明書を検証することができることを意味する.サービス側公開鍵を取得します.もちろん不足しているのは、このような証明書は有料かもしれません.
    コードは次のように実装されます.
  • HttpURLConnection(Android 4.4以降はOkHttpに置き換えられているAPI)
  •     HttpURLConnection urlConnection = null;
        try {
            URL url = new URL("https://www.baidu.com");
            urlConnection = (HttpURLConnection) url.openConnection();
            InputStream in = urlConnection.getInputStream();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
        }
    

    この場合、url.openConnection()は、urlがHTTPSフォーマットの場合、実際にはHttpsURLConnectionオブジェクトを返すため、通常のHTTPを使用するのと何の違いもありません.
  • HttpClient(Android 6.0以降はこのフレームワークを除く)
  •     HttpClient httpClient = new DefaultHttpClient();
        HttpGet httpGet = new HttpGet("https://www.baidu.com/");
        try {
            HttpResponse httpResponse = httpClient.execute(httpGet);
            if (httpResponse.getStatusLine().getStatusCode() == 200){
                HttpEntity entity = httpResponse.getEntity();
                String response = EntityUtils.toString(entity,"utf-8");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    

    同様にHTTPコードの作成と何の違いもありません.
    ここで第1の方法は、証明書を発行するCAが信頼されていること、すなわち、HTTPSプロトコルを使用するデバイスまたはブラウザが信頼されているターゲットサービス側証明書の署名機関であることを前提としている.このような場合、コードの作成は簡単であることがわかりますが、日常のAndroid開発では、このような簡単な方法が私たちのニーズをカバーできるとは限らないのはなぜですか.Androidの断片化が深刻なため、異なるバージョンの異なるデバイスが信頼しているCAのリストが異なる可能性があります.そのため、コードがこのデバイスで正常に動作していることがよくありますが、別のデバイスではjavax.net.ssl.SSLExceptionになります.
    2.未知のCA
    未知のCAとは、信頼リストにデバイスが追加されていない公的CAの3つのケースがあり、2つ目は政府や会社、教育機関などの組織が発行するプライベートCA(国内ではよく見られる12306サイトの証明書など)、3つ目は自己署名の証明書(この場合は厳密にはCAの認証ではなく、自分がKeytoolやOpenSSLを使って証明書を発行し、例えば自分が自分のCAである)である.この3つのケースは開発における処理の差が大きくないため,1つに分類される.このようなCAでは、CA証明書ファイルまたはコンテンツ文字列をローカルに内蔵するのが一般的です.
  • HttpURLConnection
  •     InputStream caInput = null;
        Certificate ca = null;
        try {
            // Load CAs from an InputStream
            // (could be from a resource or ByteArrayInputStream or ...)
            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            caInput = context.getAssets().open("srca.cer");
            ca = cf.generateCertificate(caInput);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                caInput.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        try {
            // 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 sslContext = SSLContext.getInstance("TLS");
            sslContext.init(null, tmf.getTrustManagers(), null);
            // Tell the URLConnection to use a SocketFactory from our SSLContext
            URL url = new URL("https://kyfw.12306.cn/otn/");
            HttpsURLConnection urlConnection = (HttpsURLConnection) url.openConnection();
            urlConnection.setSSLSocketFactory(sslContext.getSocketFactory());
            InputStream in = urlConnection.getInputStream();
        } catch (Exception e) {
            e.printStackTrace();
        }
    

    上のコードのsrc a.cer証明書ファイルは12306公式サイトからダウンロードされています.もちろん、証明書の内容を取得し、コードで文字列定数として定義することもできます.
  • HttpClient
  •     InputStream caInput = null;
        Certificate ca = null;
        try {
            // Load CAs from an InputStream
            // (could be from a resource or ByteArrayInputStream or ...)
            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            caInput = context.getAssets().open("srca.cer");
            ca = cf.generateCertificate(caInput);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                caInput.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        try {
            // Create a KeyStore containing our trusted CAs
            String keyStoreType = KeyStore.getDefaultType();
            KeyStore keyStore = KeyStore.getInstance(keyStoreType);
            keyStore.load(null, null);
            keyStore.setCertificateEntry("ca", ca);
            SSLSocketFactory ssl = new SSLSocketFactory(keyStore);
            ssl.setHostnameVerifier(ssl.getHostnameVerifier());
            Scheme scheme = new Scheme("https", ssl, 443);
            HttpClient httpclient = new DefaultHttpClient();
            httpclient.getConnectionManager().getSchemeRegistry().register(scheme);
            HttpGet httpGet = new HttpGet("https://kyfw.12306.cn/otn/");
            HttpResponse httpResponse = httpclient.execute(httpGet);
            if (httpResponse.getStatusLine().getStatusCode() == 200) {
                HttpEntity entity = httpResponse.getEntity();
                String response = EntityUtils.toString(entity, "utf-8");          
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    

    HttpURLConnectionと比較して、コード・ソケットはほぼ一致しており、証明書を読み込んでCertificateオブジェクトを生成し、証明書に基づいてKeyStoreオブジェクトを構築し、最後にSSLSocketFactoryオブジェクトを構築し、HTTPプロトコルに関連付けます.
    3.中間CAがない
    中間証明書発行機関が欠けているとは何ですか?実際、多くの公共CAはサーバ証明書に直接署名するのではなく、自分のルートCAを使用して中間CAに署名し、中間CAを使用して私たちの証明書発行の要求を満たし、ルートCAの漏洩リスクを低減します.では問題ですが、Androidなどのオペレーティングシステムは通常ルートCAだけを直接信頼しているので、中間CAは未知のCAになります.この問題を解決するには、サービス側の解決とクライアントの解決の2つの方法があります(Googleの公式ドキュメントを参照してHTTPSとSSLを通じてセキュリティを確保します).
  • サービス側は、サーバチェーンに中間CAを追加するようにサーバを構成し、サーバがSSL握手中にクライアントに証明書を送信するだけでなく、サーバCAおよび信頼できるルートCAに到達するために必要な任意の中間証明書を含む証明書チェーンを送信するようにする.
  • クライアントは私が言うまでもなくみんな知っているでしょう.前文に従って、他の未知のCAのように中間CAを扱っています.

  • Android開発におけるHTTPSの実現方式については、HttpURLConnectionとHttpClientの2つのフレームワークに分けて説明したが、実際にはokhttp、retrofit、volley、android-async-httpなど、他のHTTPプロトコルのオープンソースフレームワークを使用する可能性が高い.そのため、HTTPSの使用にも大きな違いはありません.せいぜいある程度パッケージされているものもありますし、ネット上の関連資料もたくさんあります.その背後にある意味を理解すれば、実現は難しくないと思います.
    また、実際のHTTPS開発において私たちが直面しているニーズや問題は奇妙である可能性があります.TrustManagerやHostnameVerifierをカスタマイズすることで実現しなければならない場合がありますが、リスクがあることを覚えておいてください.できるだけ検証(署名CAが合法かどうか、ドメイン名が一致しているかどうか、自己署名証明書が期限切れになっているかどうか)を完了しなければなりません.できる範囲内で最も安全なコードを書きます.
    この文書のすべてのコードは、個人githubのホームページで表示およびダウンロードできます.
    また、個人技術ブログ、同期更新、注目を歓迎します!転載は出典を明記してください!文の中で何か間違いがあれば、みんなに指摘してほしい.