보안 프로바이더를 업데이트하여 SSL 악용으로부터 기기 보호

Android에서는 보안 네트워크 통신을 제공하기 위해 보안 Provider가 필요합니다. 하지만, 기본 보안 프로바이더에서 취약점이 발견되는 경우가 있습니다. 이러한 취약점으로부터 기기를 보호하기 위해 Google Play 서비스에서는 기기의 보안 프로바이더를 자동으로 업데이트하여 알려진 악용으로부터 기기를 보호하는 방법을 제공합니다. Google Play 서비스 메서드를 호출하면 최신 업데이트가 있는 기기에서 앱이 실행되도록 하여 알려진 악용으로부터 보호할 수 있습니다.

예를 들어 OpenSSL(CVE-2014-0224)에서는 어느 쪽도 모르게 보안 트래픽을 복호화하는 경로 내 공격에 앱을 노출할 수 있는 취약점이 발견되었습니다. Google Play 서비스 버전 5.0에서는 수정사항을 제공하지만 이 수정사항이 설치되어 있는지 앱에서 확인해야 합니다. Google Play 서비스 메서드를 사용하면 이러한 공격으로부터 보호되는 기기에서 앱이 실행되도록 할 수 있습니다.

주의: 기기의 보안 Provider를 업데이트해도 여전히 취약한 android.net.SSLCertificateSocketFactory는 업데이트되지 않습니다. 앱 개발자는 이 지원 중단된 클래스를 사용하는 대신 암호화와 상호작용하는 상위 수준 메서드(예: HttpsURLConnection)를 사용하는 것이 좋습니다.

ProviderInstaller를 사용하여 보안 프로바이더에 패치 사용

기기의 보안 프로바이더를 업데이트하려면 ProviderInstaller 클래스를 사용합니다. 이 클래스의 installIfNeeded()(또는 installIfNeededAsync()) 메서드를 호출하여 보안 프로바이더가 최신인지 확인(필요한 경우 업데이트)할 수 있습니다. 이 섹션에서는 이러한 옵션을 개략적으로 설명합니다. 다음 섹션에서는 더 자세한 단계와 예시를 제공합니다.

installIfNeeded()를 호출하면 ProviderInstaller에서 다음을 실행합니다.

  • 기기의 Provider가 성공적으로 업데이트되면(또는 이미 최신이면) 메서드가 예외를 발생시키지 않고 반환됩니다.
  • 기기의 Google Play 서비스 라이브러리가 최신이 아닌 경우, 메서드에서 GooglePlayServicesRepairableException이 발생합니다. 그러면 앱이 이 예외를 포착하고 사용자에게 Google Play 서비스를 업데이트하라는 대화상자를 표시합니다.
  • 복구 불가능한 오류가 발생한 경우, 메서드에서 Provider의 업데이트가 불가능하다고 알리는 GooglePlayServicesNotAvailableException이 발생합니다. 그러면 앱이 이 예외를 포착하고 표준 문제 해결 흐름 다이어그램 표시 같은 일련의 적절한 작업을 선택할 수 있습니다.

installIfNeededAsync() 메서드도 동작은 비슷하지만, 예외를 발생시키는 대신 적절한 콜백 메서드를 호출하여 성공 또는 실패 여부를 나타내는 점이 다릅니다.

보안 프로바이더가 이미 최신인 경우 installIfNeeded()에 걸리는 시간은 미미합니다. 메서드에서 새 Provider를 설치해야 하면 30~50밀리초(최신 기기의 경우)에서 350밀리초(이전 기기의 경우) 정도 걸릴 수 있습니다. 사용자 환경에 영향을 미치지 않도록 하려면 다음과 같이 합니다.

  • 스레드가 네트워크 사용을 시도하기를 기다리는 대신, 스레드가 로드될 때 즉시 백그라운드 네트워킹 스레드에서 installIfNeeded()를 호출합니다. 보안 프로바이더에 업데이트가 필요하지 않으면 이 메서드는 즉시 반환되므로 메서드를 여러 번 호출하더라도 피해가 없습니다.
  • 사용자 환경이 스레드 차단의 영향을 받을 수 있는 경우(예: UI 스레드의 활동에서 호출이 발생하는 경우) 비동기 버전의 메서드인 installIfNeededAsync()를 호출합니다. 이렇게 하는 경우, 보안 통신을 시도하기 전에 작업이 완료되기를 기다려야 합니다. ProviderInstaller는 성공 신호를 보내기 위해 리스너의 onProviderInstalled() 메서드를 호출합니다.

경고: ProviderInstaller에서 업데이트된 Provider를 설치할 수 없다면 기기의 보안 프로바이더는 알려진 악용에 취약할 수 있습니다. 앱은 모든 HTTP 통신이 암호화되지 않은 것처럼 작동해야 합니다.

Provider가 업데이트되면, 보안 API(SSL API 포함)의 모든 호출이 이를 통해 라우팅됩니다. 하지만 이는 CVE-2014-0224와 같은 악용에 여전히 취약한 android.net.SSLCertificateSocketFactory에는 적용되지 않습니다.

동기 패치 사용

보안 프로바이더에 패치를 사용하는 가장 간단한 방법은 동기 메서드 installIfNeeded()를 호출하는 것입니다. 이 방법은 작업이 완료되기를 기다리는 동안 사용자 환경이 스레드 차단으로 영향을 받지 않는 경우에 적합합니다.

예를 들어 다음은 보안 프로바이더를 업데이트하는 worker를 구현한 것입니다. worker는 백그라운드로 실행되므로 보안 프로바이더가 업데이트되기를 기다리는 동안 스레드가 차단되어도 문제가 없습니다. worker는 installIfNeeded()를 호출하여 보안 프로바이더를 업데이트합니다. 메서드가 정상적으로 반환되면 worker는 보안 프로바이더가 최신 상태임을 알게 됩니다. 메서드가 예외를 발생시키면 worker가 적절한 작업을 실행할 수 있습니다(예: 사용자에게 Google Play 서비스를 업데이트하라는 메시지 표시).

Kotlin

/**
 * Sample patch Worker using {@link ProviderInstaller}.
 */
class PatchWorker(appContext: Context, workerParams: WorkerParameters): Worker(appContext, workerParams) {

  override fun doWork(): Result {
        try {
            ProviderInstaller.installIfNeeded(context)
        } catch (e: GooglePlayServicesRepairableException) {

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

            // Prompt the user to install/update/enable Google Play services.
            GoogleApiAvailability.getInstance()
                    .showErrorNotification(context, e.connectionStatusCode)

            // Notify the WorkManager that a soft error occurred.
            return Result.failure()

        } catch (e: GooglePlayServicesNotAvailableException) {
            // Indicates a non-recoverable error; the ProviderInstaller can't
            // install an up-to-date Provider.

            // Notify the WorkManager that a hard error occurred.
            return Result.failure()
        }

        // If this is reached, you know that the provider was already up to date
        // or was successfully updated.
        return Result.success()
    }
}

Java

/**
 * Sample patch Worker using {@link ProviderInstaller}.
 */
public class PatchWorker extends Worker {

  ...

  @Override
  public Result doWork() {
    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.
      GoogleApiAvailability.getInstance()
              .showErrorNotification(context, e.connectionStatusCode)

      // Notify the WorkManager that a soft error occurred.
      return Result.failure();

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

      // Notify the WorkManager that a hard error occurred.
      return Result.failure();
    }

    // If this is reached, you know that the provider was already up to date
    // or was successfully updated.
    return Result.success();
  }
}

비동기 패치 사용

보안 프로바이더를 업데이트하는 데는 최대 350밀리초의 시간이 걸릴 수 있습니다(이전 기기의 경우). 사용자 환경에 직접 영향을 미치는 스레드(예: UI 스레드)에서 업데이트를 실행하는 경우 동기 호출을 통해 프로바이더를 업데이트하려고 하지 않습니다. 동기 호출을 실행하면 작업이 완료될 때까지 앱이나 기기가 멈출 수 있기 때문입니다. 대신 비동기 메서드 installIfNeededAsync()를 사용하세요. 이 메서드는 콜백을 호출하여 성공 또는 실패 여부를 나타냅니다.

예를 들어, 다음은 UI 스레드의 활동에서 보안 프로바이더를 업데이트하는 코드입니다. 이 활동은 보안 프로바이더를 업데이트하기 위해 installIfNeededAsync()를 호출하고 성공 또는 실패 알림을 수신하기 위해 자신을 리스너로 지정합니다. 보안 프로바이더가 최신이거나 성공적으로 업데이트되면, 활동의 onProviderInstalled() 메서드가 호출되고, 이 활동은 통신이 안전하다는 것을 알게 됩니다. 프로바이더를 업데이트할 수 없는 경우, 활동의 onProviderInstallFailed() 메서드가 호출되고 활동이 적절한 작업을 취할 수 있습니다(예: Google Play 서비스를 업데이트하도록 사용자에게 메시지 표시).

Kotlin

private const val ERROR_DIALOG_REQUEST_CODE = 1

/**
 * Sample activity using {@link ProviderInstaller}.
 */
class MainActivity : Activity(), ProviderInstaller.ProviderInstallListener {

    private var retryProviderInstall: Boolean = false

    // Update the security provider when the activity is created.
    override fun onCreate(savedInstanceState: Bundle?) {
        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 fun 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 fun onProviderInstallFailed(errorCode: Int, recoveryIntent: Intent) {
        GoogleApiAvailability.getInstance().apply {
            if (isUserResolvableError(errorCode)) {
                // Recoverable error. Show a dialog prompting the user to
                // install/update/enable Google Play services.
                showErrorDialogFragment(this@MainActivity, errorCode, ERROR_DIALOG_REQUEST_CODE) {
                    // The user chose not to take the recovery action.
                    onProviderInstallerNotAvailable()
                }
            } else {
                onProviderInstallerNotAvailable()
            }
        }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int,
                                  data: Intent) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == ERROR_DIALOG_REQUEST_CODE) {
            // Adding a fragment via GoogleApiAvailability.showErrorDialogFragment
            // before the instance state is restored throws an error. So instead,
            // set a flag here, which causes the fragment to delay until
            // onPostResume.
            retryProviderInstall = true
        }
    }

    /**
     * On resume, check whether a flag indicates that the provider needs to be
     * reinstalled.
     */
    override fun onPostResume() {
        super.onPostResume()
        if (retryProviderInstall) {
            // It's safe to retry installation.
            ProviderInstaller.installIfNeededAsync(this, this)
        }
        retryProviderInstall = false
    }

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

Java

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

  private static final int ERROR_DIALOG_REQUEST_CODE = 1;

  private boolean retryProviderInstall;

  // 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) {
    GoogleApiAvailability availability = GoogleApiAvailability.getInstance();
    if (availability.isUserRecoverableError(errorCode)) {
      // Recoverable error. Show a dialog prompting the user to
      // install/update/enable Google Play services.
      availability.showErrorDialogFragment(
          this,
          errorCode,
          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 isn't 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 GoogleApiAvailability.showErrorDialogFragment
      // before the instance state is restored throws an error. So instead,
      // set a flag here, which causes the fragment to delay until
      // onPostResume.
      retryProviderInstall = true;
    }
  }

  /**
  * On resume, check whether a flag indicates that the provider needs to be
  * reinstalled.
  */
  @Override
  protected void onPostResume() {
    super.onPostResume();
    if (retryProviderInstall) {
      // It's safe to retry installation.
      ProviderInstaller.installIfNeededAsync(this, this);
    }
    retryProviderInstall = false;
  }

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