Android ネットワーク セキュリティ構成の Codelab

アプリでは一般的に、インターネットを経由してデータ交換を行っています。信頼できるサーバー以外のサーバーとも通信する可能性があるため、機密性の高い情報やプライベートな情報を送受信する場合は注意が必要です。

作成するアプリの概要

この Codelab では、メッセージを表示するアプリを作成します。各メッセージには、送信者の名前、テキスト メッセージ、「プロフィール写真」の URL が含まれます。アプリでは、次の手順でメッセージが表示されます。

  • ネットワークからテキスト メッセージのリストが含まれている JSON ファイルを読み込む。
  • プロフィール写真を読み込んで適切なメッセージの横に表示する。

学習内容

  • 安全なネットワーク通信が重要な理由。
  • Volley ライブラリを使用して、ネットワーク リクエストを行う方法。
  • ネットワーク セキュリティ構成を使用してネットワーク通信の安全性を高める方法。
  • 開発やテストの際に役立つ、高度なネットワーク セキュリティ構成オプションの変更方法。
  • ネットワーク セキュリティに関するよくある問題の 1 つを確認し、ネットワーク セキュリティ構成によってその問題を防ぐ方法。

必要なもの

  • Android Studio の最新バージョン
  • Android 7.0(API レベル 24)以降を実行する Android デバイスまたはエミュレータ
  • Node.js(または設定可能なウェブサーバーへのアクセス)

この Codelab で問題(コードのバグ、文法的な誤り、不明確な表現など)が見つかった場合は、Codelab の左下隅にある [誤りを報告] から問題を報告してください。

コードをダウンロードする

次のリンクをクリックして、この Codelab のコードをすべてダウンロードします。

ソースコードをダウンロード

ダウンロードした zip ファイルを解凍すると、ルートフォルダ(android-network-secure-config)が展開されます。ルートフォルダには、Android Studio プロジェクト(SecureConfig/)と後ほど使用するデータファイル(server/)が含まれています。

また、GitHub からコードを直接チェックアウトすることもできます(master ブランチから始めます)。

GitHub リポジトリ

また、各ステップ後の最終的なコードのブランチも用意しています。行き詰まった場合は、GitHub のブランチを参照するか、リポジトリ全体(https://github.com/googlecodelabs/android-network-security-config/branches/all)のクローンを作成してください。

このアプリは、「読み込み」アイコンをクリックすると、リモート サーバーにアクセスして JSON ファイルからメッセージ リスト、名前、プロフィール写真の URL を読み込みます。次に、メッセージがリスト表示され、アプリは参照先の URL から画像を読み込みます。

注: この Codelab で使用しているアプリは、あくまでもデモ用です。本番環境で必要とされるだけのエラー処理は含まれていません。

d9e465c94b420ea1.png

アプリ アーキテクチャ

アプリでは MVP パターンに沿って、データ ストレージとネットワーク アクセス(Model)をロジック(Presenter)とディスプレイ(View)から分離します。

MainContract クラスには、View と Presenter との間のインターフェースを記述するコントラクトが含まれています。

MainContract.java

/*
 * Copyright 2017 Google Inc. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.example.networksecurity;

import com.example.networksecurity.model.Post;

/**
 * Contract defining the interface between the View and Presenter.
 */
public interface MainContract {

    interface View {
        /**
         * Sets the presenter for interaction from the View.
         *
         * @param presenter
         */
        void setPresenter(Presenter presenter);

        /**
         * Displays or hides a loading indicator.
         *
         * @param isLoading If true, display a loading indicator, hide it otherwise.
         */
        void setLoadingPosts(boolean isLoading);

        /**
         * Displays a list of posts on screen.
         *
         * @param posts The posts to display. If null or empty, the list should not be shown.
         */
        void setPosts(Post[] posts);

        /**
         * Displays an error message on screen and optionally prints out the error to logcat.
         */
        void showError(String title, String error);

        /**
         * Hides the error message.
         *
         * @see #showError(String, String)
         */
        void hideError();

        /**
         * Displays an empty message and icon.
         *
         * @param showMessage If true, the message is show. If false, the message is hidden
         */
        void showNoPostsMessage(boolean showMessage);
    }

    interface Presenter {
        /**
         * Call to start the application. Sets up initial state.
         */
        void start();

        /**
         * Loads post for display.
         */
        void loadPosts();

        /**
         * An error was encountered during the loading of profile images.
         */
        void onLoadPostImageError(String error, Exception e);
    }

}

アプリの構成

わかりやすくするために、このアプリではネットワーク キャッシュをすべて無効にしています。本番環境では、ローカル キャッシュを使用してリモート ネットワーク リクエストの数を制限することをおすすめします。

gradle.properties ファイルには、メッセージ リストの読み込み元となる URL が含まれています。

gradle.properties

postsUrl="http://storage.googleapis.com/network-security-conf-codelab.appspot.com/v1/posts.json"

アプリをビルドして実行する

  1. Android Studio を起動し、SecureConfig ディレクトリを Android プロジェクトとして開きます。
  2. 実行アイコンをクリックしてアプリを起動します。e15973f44eed7cc2.png

以下のスクリーンショットは、アプリがデバイス上でどう表示されるかを示しています。

63300e7e262bd161.png

このステップでは、基本的なネットワーク セキュリティ構成を設定し、構成内のルールのいずれかに違反したときに発生するエラーを確認します。

概要

ネットワーク セキュリティ構成により、宣言型構成ファイルを使用してアプリのネットワーク セキュリティ設定をカスタマイズできます。構成全体がこの XML ファイル内に含まれているため、コードを変更する必要はありません。

カスタマイズにより、以下の構成が可能になります。

  • クリアテキスト トラフィックのオプトアウト: クリアテキスト トラフィックを無効にします。
  • カスタム トラスト アンカー: アプリが信頼する認証局とソースを指定します。
  • デバッグ限定のオーバーライド: リリースビルドに影響を与えずに、セキュアな接続を安全にデバッグできます。
  • 証明書のピン留め: セキュアな接続を特定の証明書に限定します。

このファイルはドメインごとに整理して、ネットワーク セキュリティ設定をすべての URL に適用することも、特定のドメインのみに適用することもできます。

ネットワーク セキュリティ構成は Android 7.0(API レベル 24)以降で使用できます。

ネットワーク セキュリティ構成の XML ファイルを作成する

新しい XML リソース ファイルnetwork_security_config.xml という名前で作成します。

左側の Android プロジェクト パネルres を右クリックし、[New] > [Android Resource File] を選択します。

35db6786b96a6980.png

以下のオプションを設定して [OK] をクリックします。

File name(ファイル名)

network_security_config.xml

Resource type(リソースの種類)

XML

Root element(ルート要素)

network-security-config

Directory name(ディレクトリ名)

xml

36ae9e950fe66f1c.png

xml/network_security_config.xml ファイルを開きます(自動的に開かない場合)。

内容を以下のスニペットで置き換えます。

res/xml/network_security_config.xml

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="false" >
    </base-config>
</network-security-config>

この構成は、アプリの基本構成(デフォルトのセキュリティ構成)に適用され、すべてのクリアテキスト トラフィックを無効にします

ネットワーク セキュリティ構成を有効にする

次に、アプリ構成への参照を AndroidManifest.xml ファイルに追加します。

AndroidManifest.xml ファイルを開き、ファイル内の application 要素を探します。

まず、android:usesCleartextTraffic="true" プロパティを設定する行を削除します。

次に、AndroidManifest の application 要素に android:networkSecurityConfig プロパティを追加し、network_security_config XML ファイル リソース(@xml/network_security_config)を参照します。

上記の 2 つのプロパティを削除、追加すると、開始アプリケーション タグは以下のようになります。

AndroidManifest.xml

<application
    android:networkSecurityConfig="@xml/network_security_config"
    android:allowBackup="true"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/AppTheme"
    android:fullBackupContent="false"
    tools:ignore="GoogleAppIndexingWarning">
    ...

アプリをコンパイルして実行する

アプリをコンパイルして実行します。

するとエラーが発生します。アプリでクリアテキスト接続でデータを読み込もうとしているからです。

98d8a173d5293742.png

logcat には、以下のようなエラーが表示されます。

java.io.IOException: Cleartext HTTP traffic to storage.googleapis.com not permitted

暗号化されていない HTTP 接続からメッセージのリストを読み込む設定のままであるため、データは読み込まれません。gradle.properties ファイルに設定されている URL は TLS を使用しない HTTP サーバーを指しています。

この URL を変更して、別のサーバーを使用して安全な HTTPS 接続でデータを読み込むように設定を変更しましょう。

gradle.properties ファイルを以下のように変更します。

gradle.properties

postsUrl="https://storage.googleapis.com/network-security-conf-codelab.appspot.com/v1/posts.json"

(URL が https プロトコルを使用していることに注目してください)

この変更を反映させるために、プロジェクトの再構築が必要になる場合があります。メニューから Build > Rebuild を選択します。

アプリを再度実行すると、ネットワーク リクエストで HTTPS 接続が使用されているため、データが読み込まれるのを確認できます。

63300e7e262bd161.png

ネットワーク セキュリティ構成により、保護されていない接続でリクエストを行う際の脆弱性からアプリを保護できます。

ネットワーク セキュリティ構成で対処できるもう 1 つの問題は、Android アプリに読み込まれる URL に影響するサーバー側の変更です。たとえば、プロフィール写真の URL として、安全な HTTPS URL ではなく、安全でない HTTP URL をサーバーが返すようになったとします。HTTPS 接続を強制するネットワーク セキュリティ構成の場合、実行時にこの要件が満たされないため、例外が発生します。

アプリ バックエンドを更新する

すでに説明したように、このアプリはまずメッセージのリストを読み込みます。メッセージはそれぞれプロフィール写真の URL を参照します。

アプリが使用するデータが変更されたために、アプリが別の画像 URL をリクエストするようになったとします。バックエンド データの URL を変更して、この変更をシミュレーションしてみましょう。

gradle.properties ファイルを以下のように変更します。

gradle.properties

postsUrl="https://storage.googleapis.com/network-security-conf-codelab.appspot.com/v2/posts.json"

(パスに「v2」が含まれている点に注目してください)

この変更を反映させるために、プロジェクトの再構築が必要になる場合があります。メニューから Build > Rebuild を選択します。

ブラウザから「新しい」バックエンドにアクセスし、変更された JSON ファイルを表示できます。参照されているすべての URL で使用されているのは、HTTPS ではなく HTTP です。

アプリを実行してエラーを調べる

アプリをコンパイルして実行します。

メッセージは読み込まれますが、画像は読み込まれていません。アプリと logcat のエラー メッセージを調べて、理由を確認します。

a2a98a842e99168d.png

java.io.IOException: Cleartext HTTP traffic to storage.googleapis.com not permitted

前と同じように、JSON ファイルへのアクセスに HTTPS が使用されています。しかし、JSON ファイル内のプロフィール写真のリンクには HTTP アドレスが使用されているため、アプリは(安全でない)HTTP で画像を読み込もうとします。

データの保護

ネットワーク セキュリティ構成により、誤ってデータが流出することを防ぐことができました。アプリでは、保護されていないデータへのアクセスを試みることはせず、接続の試行をブロックします。

ロールアウト前にバックエンドの変更が十分にテストされていない場合のようなシナリオを考えてみてください。Android アプリにネットワーク セキュリティ構成を適用することで、アプリのリリース後も同様の問題が発生しないようにできます。

バックエンドを変更してアプリを修正する

バックエンド URL を「修正された」新しい URL に変更します。この例では、適切な HTTPS URL でプロフィール写真を参照することで、修正をシミュレーションしています。

gradle.properties ファイルのバックエンド URL を変更してプロジェクトを更新します。

gradle.properties

postsUrl="https://storage.googleapis.com/network-security-conf-codelab.appspot.com/v3/posts.json"

(パスに v3 が含まれている点に注目してください)

アプリを再度実行すると、以下のように意図したとおりに動作するようになりました。

63300e7e262bd161.png

ここまでは、ネットワーク セキュリティ構成を base-config で指定してきました。これにより、アプリが確立しようとするすべての接続にこの構成が適用されます。

この構成は、domain-config 要素を指定することで、特定の宛先に対してオーバーライドできます。domain-config では、特定のドメインセットについて構成オプションを宣言します。

アプリのネットワーク セキュリティ構成を、次のように更新しましょう。

res/xml/network_security_config.xml

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="false" />
    <domain-config cleartextTrafficPermitted="true">
        <domain includeSubdomains="true">localhost</domain>
    </domain-config>
</network-security-config>

この構成では、base-config がすべてのドメインに適用されます。ただし、ドメイン「localhost」とそのサブドメインには別の構成が適用されます。

基本構成はすべてのドメインのクリアテキスト トラフィックを防ぎます。しかし、ドメイン構成によってこのルールをオーバーライドすることで、アプリがクリアテキストを使用して localhost にアクセスできるようになります。

ローカル HTTP サーバーを使用してテストする

アプリでクリアテキストを使用して localhost にアクセスできるようになったため、ローカル ウェブサーバーを起動し、このアクセス プロトコルをテストしましょう。

ごく基本的なウェブサーバーをホストするためには、Node.JS、Python、Perl など、さまざまなツールを使用できます。この Codelab では、http-server Node.JS モジュールを使用してアプリのデータを配信します。

  1. ターミナルを開いて、以下のように http-server をインストールします。
npm install http-server -g
  1. コードをチェックアウトしたディレクトリに移動し、次に server/ ディレクトリに移動します。
cd server/
  1. ウェブサーバーを起動し、data/ ディレクトリ内のファイルを配信します。
http-server ./data -p 8080
  1. ウェブブラウザを開き、http://localhost:8080 に移動して、ファイルにアクセスできることを確認し、「posts.json」ファイルを表示します。

934e48553bcc48e7.png

  1. 次に、ポート 8080 をデバイスからローカルマシンに転送します。別のターミナル ウィンドウで以下のコマンドを実行します。
adb reverse tcp:8080 tcp:8080

これで、アプリが Android デバイスから「localhost:8080」にアクセスできるようになりました。

  1. アプリでデータを読み込むための URL を変更して、localhost の新しいサーバーを指すようにします。gradle.properties ファイルを以下のように変更します(このファイルを変更した後に、必ず Gradle プロジェクトの同期を行う必要があります)。

gradle.properties

postsUrl="http://localhost:8080/posts.json"
  1. アプリを実行して、データがローカルマシンから読み込まれることを確認します。data/posts.json ファイルを変更し、アプリを更新して、新しい構成が想定どおりに機能しているか確認できます。

63300e7e262bd161.png

補足 - ドメインの構成

特定のドメインに適用される構成オプションは、domain-config 要素で定義します。この要素には複数の domain エントリを含めることができます。このエントリには、domain-config ルールを適用すべき場所を指定します。複数の domain-config 要素に類似した domain エントリが含まれている場合、ネットワーク セキュリティ構成によって、一致する文字の数に基づいて、特定の URL に適用される構成が選択されます。選択されるのは、特定の URL と最も多くの文字が連続して一致する domain エントリを含む構成です。

ドメイン構成は複数のドメインに適用できるうえ、サブドメインを含めることもできます。

以下の例は、複数のドメインを含むネットワーク セキュリティ構成を示します(アプリの変更はありません。これはあくまでも一例です)。

<network-security-config>
    <domain-config>
        <domain includeSubdomains="true">secure.example.com</domain>
        <domain includeSubdomains="true">cdn.example.com</domain>
        <trust-anchors>
            <certificates src="@raw/trusted_roots"/>
        </trust-anchors>
    </domain-config>
</network-security-config>

詳細については、構成ファイル形式の定義をご覧ください。

HTTPS でリクエストを行うように設計されたアプリを開発してテストする際には、前のステップで行ったように、ローカルのウェブサーバーやテスト環境への接続が必要になる場合があります。

このようなユースケースの場合、クリアテキスト トラフィックの使用を全面的に許可したり、コードを変更したりするのではなく、ネットワーク セキュリティ構成で debug-override オプションを使用することで、アプリがデバッグモードで実行されるとき、つまり android:debuggable が true のときにのみ適用されるセキュリティ オプションを設定できます。これは明示的なデバッグ専用の定義があるため、条件付きコードを使用するよりもはるかに安全です。また、Play ストアでは、デバッグ可能なアプリはアップロードされないようになっているため、このオプションの安全性はさらに高まります。

ローカル ウェブサーバーで SSL を有効にする

先ほど、ローカル ウェブサーバーを起動し、ポート 8080 から HTTP でデータを配信しました。ここでは、自己署名 SSL 証明書を生成し、その証明書を使用して HTTPS でデータを配信します。

  1. ターミナル ウィンドウで server/ ディレクトリに移動し、以下のコマンドを実行して証明書を生成します(HTTP サーバーをまだ起動している場合は、[CTRL] + [C] を押して停止できます)。
# Run these commands from inside the server/ directory!

# Create a certificate authority
openssl genrsa -out root-ca.privkey.pem 2048
# Sign the certificate authority
openssl req -x509 -new -nodes -days 100 -key root-ca.privkey.pem -out root-ca.cert.pem -subj "/C=US/O=Debug certificate/CN=localhost" -extensions v3_ca -config openssl_config.txt
# create DER format crt for Android
openssl x509 -outform der -in root-ca.cert.pem -out debug_certificate.crt

これにより、認証局が生成され、署名が行われて、Android に必要な DER 形式の証明書が生成されます。

  1. 新しく生成された証明書を使用して、HTTPS でウェブサーバーを起動します。
http-server ./data --ssl --cert root-ca.cert.pem --key root-ca.privkey.pem

バックエンド URL を更新する

HTTPS で localhost サーバーにアクセスするようにアプリを変更します。

以下のように gradle.properties ファイルを変更します。

gradle.properties

postsUrl="https://localhost:8080/posts.json"

アプリをコンパイルして実行します。

サーバーの証明書が無効であるため、アプリはエラーで失敗します。

3bcce1390e354724.png

java.security.cert.CertPathValidatorException: Trust anchor for certification path not found.

システムの一部として信頼されていない自己署名証明書がサーバーで使用されているため、アプリはウェブサーバーにアクセスできません。次のステップでは、HTTPS を無効にするのではなく、この自己署名証明書を localhost ドメイン用に追加します。

カスタム認証局を参照する

ウェブサーバーは、自己署名認証局(CA)を使用してデータを配信するようになりましたが、デフォルトでどのデバイスにも受け入れられません。ブラウザからサーバーにアクセスすると、https://localhost:8080 に関するセキュリティ警告が表示されます。

898b69ea4fe9bc21.png

次に、ネットワーク セキュリティ構成で debug-overrides オプションを使用して、このカスタム認証局を localhost ドメインに対してのみ許可します。

  1. xml/network_security_config.xml ファイルを以下の内容に変更します。

res/xml/network_security_config.xml

<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
    <base-config cleartextTrafficPermitted="false" />
    <debug-overrides>
        <trust-anchors>
            <!-- Trust a debug certificate in addition to the system certificates -->
            <certificates src="system" />
            <certificates src="@raw/debug_certificate" />
        </trust-anchors>
    </debug-overrides>
</network-security-config>

この構成では、クリアテキストのネットワーク トラフィックが無効化され、デバッグビルド* の場合は、システムによって提供される認証局と、res/raw ディレクトリに保存されている証明書ファイルが有効になります。

注: デバッグ構成では暗黙的に <certificates src="system" /> が追加されるため、このコードでなくてもアプリは動作します。これを追加したのは、より高度な構成でコードを追加する方法を示すためです。

  1. 次に Android Studio で、server/ ディレクトリにある「debug_certificate.crt」ファイルを本アプリの res/raw リソース ディレクトリにコピーします。この操作は、Android Studio でこのファイルを適切な場所にドラッグ&ドロップすることでも実施できます。

上記ディレクトリが存在しないときは、先に作成しておく必要があります。

これを行うには、server/ ディレクトリから以下のコマンドを実行します。あるいは、ファイル マネージャーや Android Studio を使用してフォルダを作成し、ファイルを適切な場所にコピーします。

mkdir  ../SecureConfig/app/src/main/res/raw/
cp debug_certificate.crt ../SecureConfig/app/src/main/res/raw/

Android Studio で app/res/raw の下に debug_certificate.crt ファイルが表示されるようになります。

c3111ae17558e167.png

アプリを実行する

アプリをコンパイルして実行すると、アプリは自己署名デバッグ用証明書を使用して HTTPS でローカル ウェブサーバーにアクセスします。

エラーが発生した場合は、logcat の出力をよく確認し、新しいコマンドライン オプションを使用して http-server を再起動していることを確認してください。また、debug_certificate.crt ファイルが適切な場所(res/raw/debug_certificate.crt)にあることを確認します。

63300e7e262bd161.png

ネットワーク セキュリティ構成は、その他にも以下のような高度な機能を多数サポートしています。

こうした機能を使用する際のおすすめの設定と制限事項の詳細については、ドキュメントをご覧ください。

アプリのセキュリティを強化する

この Codelab の一環として、ネットワーク セキュリティ構成を使用して Android アプリのセキュリティを強化する方法を学習しました。アプリでこうした機能を活用する方法、テストや開発で強力なデバッグ構成を活用する方法について考えてみてください。

詳細