Android Dev Summit, October 23-24: two days of technical content, directly from the Android team. Sign-up for livestream updates.

보안 프로바이더를 업데이트하여 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 서비스를 업데이트하라는 대화상자를 표시합니다.
  • 복구 불가능한 오류가 발생하는 경우 메서드는 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()를 호출하는 것입니다. 이 방법은 작업이 완료되기를 기다리는 동안 사용자 환경이 스레드 차단으로 영향을 받지 않는 경우에 적합합니다.

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

Kotlin

    /**
     * Sample sync adapter using {@link ProviderInstaller}.
     */
    class SyncAdapter(context: Context) : AbstractThreadedSyncAdapter(context, true) {

        override fun onPerformSync(
                account: Account,
                extras: Bundle,
                authority: String,
                provider: ContentProviderClient,
                syncResult: SyncResult
        ) {
            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 SyncManager that a soft error occurred.
                syncResult.stats.numIoExceptions++
                return

            } catch (e: GooglePlayServicesNotAvailableException) {
                // 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.
        }
    }
    

자바

    /**
     * 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.
          GoogleApiAvailability.getInstance()
                  .showErrorNotification(context, e.connectionStatusCode)

          // 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 서비스를 업데이트하라는 메시지 표시).

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 will cause the fragment to delay until
                // onPostResume.
                retryProviderInstall = true
            }
        }

        /**
         * On resume, check to see if we flagged that we need to reinstall the
         * provider.
         */
        override fun onPostResume() {
            super.onPostResume()
            if (retryProviderInstall) {
                // We can now safely retry installation.
                ProviderInstaller.installIfNeededAsync(this, this)
            }
            retryProviderInstall = false
        }

        private fun 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.
        }
    }
    

자바

    /**
     * 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 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 GoogleApiAvailability.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.
          retryProviderInstall = true;
        }
      }

      /**
       * On resume, check to see if we flagged that we need to reinstall the
       * provider.
       */
      @Override
      protected void onPostResume() {
        super.onPostResume();
        if (retryProviderInstall) {
          // We can now safely retry installation.
          ProviderInstaller.installIfNeededAsync(this, this);
        }
        retryProviderInstall = 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.
      }
    }