SSL の脆弱性攻撃から保護するためにセキュリティ プロバイダをアップデートする

Android ではセキュリティ Provider を使用して、安全なネットワーク通信を実現しています。ただし、デフォルトのセキュリティ プロバイダに脆弱性が見つかることがあります。これらの脆弱性から保護するために、Google Play サービスは、端末のセキュリティ プロバイダを自動的にアップデートし、既知の脆弱性攻撃を防いでいます。アプリで Google Play サービスのメソッドを呼び出すことにより、セキュリティ プロバイダが最新の状態にアップデートされた端末上でアプリが実行され、既知の脆弱性攻撃からの保護が保証されます。

たとえば、OpenSSL(CVE-2014-0224)で見つかった脆弱性により、アプリは man-in-the-middle 攻撃にさらされるリスクがあります。この攻撃では、サーバー側とクライアント側の両方に知られることなく、安全なトラフィックの暗号化が解除されます。Google Play サービス のバージョン 5.0 では、修正プログラムが利用できます。アプリでは、この修正プログラムがインストールされていることを確認する必要があります。Google Play サービスのメソッドを使用すると、この攻撃から保護された端末上でアプリを実行することができます。

警告: 端末のセキュリティ Provider をアップデートしても、android.net.SSLCertificateSocketFactory はアップデートされません。アプリ デベロッパーには、このクラスを使用する代わりに、暗号化を処理するための高レベルのメソッドを使用することをお勧めします。ほとんどのアプリは、カスタム TrustManager を設定したり、SSLCertificateSocketFactory を作成したりせずに、HttpsURLConnection などの API を使用することができます。

ProviderInstaller でセキュリティ プロバイダにパッチを適用する

端末のセキュリティ プロバイダをアップデートするには、ProviderInstaller クラスを使用します。このクラスの installIfNeeded()(または installIfNeededAsync())メソッドを呼び出すことにより、セキュリティ プロバイダが最新の状態であることを確認できます(または、必要に応じて、セキュリティ プロバイダをアップデートできます)。

installIfNeeded() を呼び出すと、ProviderInstaller により次の処理が行われます。

  • 端末の Provider が正常にアップデートされている場合(または既に最新の状態である場合)、このメソッドは正常に結果を返します。
  • 端末の Google Play サービス ライブラリが古い場合、このメソッドは GooglePlayServicesRepairableException をスローします。次に、アプリはこの例外を捕捉し、Google Play サービスをアップデートするための適切なダイアログ ボックスをユーザーに表示します。
  • 修復不可能なエラーが発生した場合、このメソッドは GooglePlayServicesNotAvailableException をスローし、Provider をアップデートできないことを示します。次に、アプリはこの例外を捕捉し、標準の修正フロー図を表示するなどの適切なアクションを実行します。

例外をスローせずに、適切なコールバック メソッドを呼び出して成功または失敗を示すことを除いて、installIfNeededAsync() メソッドは同じように機能します。

installIfNeeded() で新しい Provider をインストールする必要がある場合、インストールには 30~50 ミリ秒(新しい端末の場合)から 350 ミリ秒(以前の端末の場合)程度かかります。セキュリティ プロバイダが既に最新の状態である場合、このメソッドは瞬時に実行されます。ユーザー エクスペリエンスに影響を与えないようにするには、次の処理を実行します。

  • スレッドがネットワークの使用を試みるのを待つ代わりに、バックグラウンド ネットワーク スレッドが読み込まれると即座に、これらのスレッドから installIfNeeded() を呼び出します(セキュリティ プロバイダをアップデートする必要がない場合、このメソッドは結果を即座に返すため、このメソッドを複数回呼び出しても悪影響を及ぼすことはありません)。
  • ユーザー エクスペリエンスがスレッド ブロックの影響を受ける場合、たとえば、UI スレッドのアクティビティからの呼び出しの場合、このメソッドの非同期バージョン installIfNeededAsync() を呼び出します(当然ながら、この非同期バージョンを呼び出す場合は、安全な通信を試行する前に、この操作が完了するのを待つ必要があります。ProviderInstaller はリスナーの onProviderInstalled() メソッドを呼び出し、成功したことを示します)。

警告: ProviderInstaller がアップデート済みの Provider をインストールできない場合、端末のプロバイダが既知の脆弱性攻撃に対して無防備になる可能性があります。アプリは、すべての HTTP 通信が暗号化されていないかのように動作します。

Provider がアップデートされると、Security API(SSL API を含む)呼び出しはすべて Provider を経由します(ただし、これにより、android.net.SSLCertificateSocketFactory が適用されることはありません。つまり、CVE-2014-0224 に記載された脆弱性攻撃などに対しては無防備のままです)。

同期的にパッチを適用する

セキュリティ プロバイダにパッチを適用する最も簡単な方法は、同期メソッド installIfNeeded() を呼び出すことです。この方法は、操作の完了を待機しているときに、スレッド ブロックがユーザー エクスペリエンスに影響を及ぼさない場合に適切です。

例として、セキュリティ プロバイダをアップデートする同期アダプタが実装される場合について説明します。同期アダプタはバックグラウンドで実行されるため、セキュリティ プロバイダがアップデートされるのを待機しているときに、スレッドがブロックされても問題ありません。同期アダプタは installIfNeeded() を呼び出して、セキュリティ プロバイダをアップデートします。このメソッドが正常に結果を帰す場合、同期アダプタでは、セキュリティ プロバイダが最新の状態であることが認識されます。このメソッドが例外をスローする場合、同期アダプタは適切なアクション(ユーザーに Google Play サービスのアップデートを求めることなど)を実行します。

/**
 * Sample sync adapter using {@link ProviderInstaller}.
 */
public class SyncAdapter extends AbstractThreadedSyncAdapter {

  ...

  // This is called each time a sync is attempted; this is okay, since the
  // overhead is negligible if the security provider is up-to-date.
  @Override
  public void onPerformSync(Account account, Bundle extras, String authority,
      ContentProviderClient provider, SyncResult syncResult) {
    try {
      ProviderInstaller.installIfNeeded(getContext());
    } catch (GooglePlayServicesRepairableException e) {

      // Indicates that Google Play services is out of date, disabled, etc.

      // Prompt the user to install/update/enable Google Play services.
      GooglePlayServicesUtil.showErrorNotification(
          e.getConnectionStatusCode(), getContext());

      // Notify the SyncManager that a soft error occurred.
      syncResult.stats.numIOExceptions++;
      return;

    } catch (GooglePlayServicesNotAvailableException e) {
      // Indicates a non-recoverable error; the ProviderInstaller is not able
      // to install an up-to-date Provider.

      // Notify the SyncManager that a hard error occurred.
      syncResult.stats.numAuthExceptions++;
      return;
    }

    // If this is reached, you know that the provider was already up-to-date,
    // or was successfully updated.
  }
}

非同期的にパッチを適用する

セキュリティ プロバイダのアップデートには、最大で 350 ミリ秒(以前の端末の場合)かかることがあります。ユーザー エクスペリエンスに直接影響を及ぼすスレッド(UI スレッドなど)でアップデートを実行する場合、同期呼び出しを実行してプロバイダのアップデートを試みると、操作が完了するまでアプリまたは端末がフリーズする可能性があるため、この操作は実行しない方がよいでしょう。その代わりに、非同期メソッド installIfNeededAsync() を使用してください。このメソッドは、コールバックを呼び出すことにより、アップデートが成功または失敗したことを示します。

たとえば、以下のコードは、UI スレッドのアクティビティでセキュリティ プロバイダをアップデートします。このアクティビティは installIfNeededAsync() を呼び出して、プロバイダをアップデートし、成功または失敗の通知を受け取るためのリスナーとしてアクティビティ自身を指定します。セキュリティ プロバイダが最新の状態であるか、正常にアップデートされた場合、アクティビティの onProviderInstalled() メソッドが呼び出され、アクティビティで通信が安全であることが認識されます。プロバイダをアップデートできない場合、アクティビティの onProviderInstallFailed() メソッドが呼び出され、アクティビティが適切なアクション(ユーザーに Google Play サービスのアップデートを求めることなど)を実行します。

/**
 * Sample activity using {@link ProviderInstaller}.
 */
public class MainActivity extends Activity
    implements ProviderInstaller.ProviderInstallListener {

  private static final int ERROR_DIALOG_REQUEST_CODE = 1;

  private boolean mRetryProviderInstall;

  //Update the security provider when the activity is created.
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ProviderInstaller.installIfNeededAsync(this, this);
  }

  /**
   * This method is only called if the provider is successfully updated
   * (or is already up-to-date).
   */
  @Override
  protected void onProviderInstalled() {
    // Provider is up-to-date, app can make secure network calls.
  }

  /**
   * This method is called if updating fails; the error code indicates
   * whether the error is recoverable.
   */
  @Override
  protected void onProviderInstallFailed(int errorCode, Intent recoveryIntent) {
    if (GooglePlayServicesUtil.isUserRecoverableError(errorCode)) {
      // Recoverable error. Show a dialog prompting the user to
      // install/update/enable Google Play services.
      GooglePlayServicesUtil.showErrorDialogFragment(
          errorCode,
          this,
          ERROR_DIALOG_REQUEST_CODE,
          new DialogInterface.OnCancelListener() {
            @Override
            public void onCancel(DialogInterface dialog) {
              // The user chose not to take the recovery action
              onProviderInstallerNotAvailable();
            }
          });
    } else {
      // Google Play services is not available.
      onProviderInstallerNotAvailable();
    }
  }

  @Override
  protected void onActivityResult(int requestCode, int resultCode,
      Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == ERROR_DIALOG_REQUEST_CODE) {
      // Adding a fragment via GooglePlayServicesUtil.showErrorDialogFragment
      // before the instance state is restored throws an error. So instead,
      // set a flag here, which will cause the fragment to delay until
      // onPostResume.
      mRetryProviderInstall = true;
    }
  }

  /**
   * On resume, check to see if we flagged that we need to reinstall the
   * provider.
   */
  @Override
  protected void onPostResume() {
    super.onPostResult();
    if (mRetryProviderInstall) {
      // We can now safely retry installation.
      ProviderInstall.installIfNeededAsync(this, this);
    }
    mRetryProviderInstall = false;
  }

  private void onProviderInstallerNotAvailable() {
    // This is reached if the provider cannot be updated for some reason.
    // App should consider all HTTP communication to be vulnerable, and take
    // appropriate action.
  }
}