Пост

Android. Защищенное соединение

Введение

На одном из последних проектов возникла потребность детальнее поразбираться в сертификатах и защищенных соединениях, чтобы локализовать проблему при установлении соединения с тестовым сервером. Так появилась эта статья, в которой разберем, как обезопасить данные Android-приложения при их передаче через Интернет, коснемся протоколов SSL/TLS и узнаем, как правильно настроить HTTPS соединения в Android-приложении.

Что такое SSL/TLS?

SSL (Secure Sockets Layer) и его преемник TLS (Transport Layer Security) обеспечивают защищенное соединение между клиентом и сервером. В Android разработчики могут использовать SSL/TLS для шифрования данных, передаваемых по сети, что особенно важно при взаимодействии с личными данными, платежной информацией и другой конфиденциальной информацией.

Когда требуется особое погружение в тему SSL/TLS:

  • Работа с self-signed сертификатами — иногда необходимо разрешить вашему приложению подключаться к серверам с самостоятельно подписанными сертификатами.
  • Пиннинг сертификатов — для защиты от атак “Человек посередине” (man in the middle), когда приложение должно принимать только определенные сертификаты.
  • Соединение через прокси-серверы, которые могут требовать полностью другой метод проверки сертификатов.
  • Требования к алгоритмам шифрования — некоторые приложения или регуляторы могут требовать использования конкретных алгоритмов шифрования.

Почему это важно для Android-приложений?

Android-приложения часто передают конфиденциальные данные и другие чувствительные данные для входа в систему. Последние несколько месяцев я работаю над связанным с банком приложением (хотя само оно к банку отношения не имеет), поэтому в нем оказалось довольно много различных нюансов, связанных с безопасностью, в частности с безопасным сетевым взаимодействием в клиент-серверной архитектуре.

Как работает SSL/TLS?

Процесс установления SSL/TLS соединения известен как “рукопожатие” (handshake). Вот его ключевые шаги:

  1. Инициализация соединения: Клиент отправляет запрос на установление соединения с сервером.
  2. Представление сертификата: Сервер предоставляет свой публичный SSL/TLS сертификат.
  3. Проверка сертификата: Клиент проверяет сертификат через центр сертификации (CA) для подтверждения его достоверности.
  4. Зашифрованная сессия: После проверки клиент и сервер согласовывают уникальный сессионный ключ для шифрования данных.

Теперь непосредственно к Android части. Рассмотрим пару сценариев использования

Сценарий 1: Работа с self-signed сертификатами

Такие сертификаты не доверенные и могут использоваться для внутренних целей или при разработке. Так как они не выпущены известным центром сертификации (CA), для их использования нужна специальная настройка.

Сценарий 2: Использование Network Security Config для фингерпринтинга сертификатов

Для повышения уровня безопасности можно использовать фингерпринтинг (сверка отпечатков сертификатов) сертификатов в конфигурации Network Security Config, что делает приложение устойчивым к атакам типа “Человек посередине” (man in the middle).

Network Security Config

С Android 7 (24 API) появилась возможность конфиг, который позволяет настраивать политики безопасности сети через XML-конфигурацию в ресурсах приложения, который также декларируется в манифесте.

1
2
3
4
5
6
7
8
9
<network-security-config>
    <domain-config>
        <domain includeSubdomains="true">host.ru</domain>
        <pin-set>
            <pin digest="SHA-256">код серта</pin>
            <!-- Пины для сертификатов -->
        </pin-set>
    </domain-config>
</network-security-config>

Этот файл определяет безопасность соединения для конкретных доменов, включая возможность пиннинга сертификатов.

Пример

В примере ниже мы создаем OkHttpClient с пиннингом сертификата, который будет доверять только определенному сертификату. Это особенно полезно при повышении безопасности сетевых запросов и защите от атак “Человек посередине”, и такая схема используется у меня в компании. В примере предполагается, что указанный InputStream certificateInputStream содержит сам сертификат, который мы хотим зафиксировать. Например, в моем случае серты лежат в raw ресурсах проекта.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import java.security.cert.CertificateFactory

fun createOkHttpClientWithPinnedCertificate(certificateInputStream: InputStream): OkHttpClient {
  val certificateFactory = CertificateFactory.getInstance("X.509")

  // Создадим сертификат из InputStream
  val certificate = certificateFactory.generateCertificate(certificateInputStream)

  // Подготовка KeyStore, включая наш сертификат
  val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()).apply {
    load(null, null)
    setCertificateEntry("ca", certificate)
  }

  // Инициализируем TrustManagerFactory с KeyStore
  val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
    .apply {
        init(keyStore)
    }

  // Получаем X509TrustManager из TrustManagerFactory
  val trustManager = trustManagerFactory.trustManagers[0] as X509TrustManager

  // Инициализируем новый SSLContext, используя TrustManager для проверки сертификатов
  val sslContext = SSLContext.getInstance("TLS")
    .apply {
        init(null, arrayOf(trustManager), null)
  }

  // Создаем OkHttpClient и устанавливаем SSLSocketFactory с нашим sslContext
  return OkHttpClient.Builder()
    .sslSocketFactory(sslContext.socketFactory, trustManager)
    .build()
}

// Использование созданного OkHttpClient для выполнения запроса
fun makeSecureRequest() {
  val certificateInputStream: InputStream = /* Загружаем сертификаты */
  val client = createOkHttpClientWithPinnedCertificate(certificateInputStream)

  val request = Request.Builder()
    .url("https://host.ru")
    .build()

  client.newCall(request).execute().use { response ->
    if (!response.isSuccessful) throw IOException("Exception: ${response.code}")

    // Обработка ответа...
  }
}

Один из ключевых моментов здесь - это TrustManager, рассмотрим его подробнее. TrustManager в контексте SSL/TLS в Java и Android отвечает за управление доверием для SSL соединений. Это элемент безопасности, который принимает решения о том, следует ли доверять предоставленным в процессе установления соединения сертификатам.

Роль и использование TrustManager:

  1. Проверка Сертификатов: Основная задача TrustManager заключается в проверке цепочки сертификатов, представленной сервером во время установления SSL/TLS соединения. Это обеспечивает, что соединение происходит с тем сервером, на который оно предназначалось, и что сервер является доверенным и подлинным.

  2. Управление Доверием: TrustManager использует хранилище доверенных сертификатов (TrustStore), чтобы решить, доверять ли удаленному серверу. TrustStore обычно содержит сертификаты известных центров сертификации (CA), с помощью которых выполняется валидация представленных сертификатов.

  3. Настраиваемая Логика Доверия: Можно создать свою реализацию TrustManager, если необходимо использовать специальную логику доверия. Например, для приложений, которые должны доверять только определенным сертификатам или которые работают с self-signed сертификатами.

  4. Интеграция с SSLContext: TrustManager должен быть инициализирован и использован для создания SSLContext. SSLContext используется для создания SSLSocketFactory, который уже применяется в сетевых подключениях для обеспечения SSL/TLS защиты.

В качестве заключения

Заключение получилось больше для себя, но все же:) Работая над статьей, лучшие погрузился в тему и нашел ответы на некоторые вопросы, которые были непонятны на проекте. Например, пиннинг сертификатов и Network Security Config оставались чем-то не до конца ясным. Надеюсь, кому-нибудь статья тоже будет полезной.

Авторский пост защищен лицензией CC BY 4.0 .

Популярные теги