Encrypted Client Hello (ECH) — это расширение TLS, которое шифрует поле Server Name Indication (SNI) в сообщении рукопожатия клиента. В Android 17 (уровень API 37) и выше ECH поддерживается по умолчанию. ECH помогает обеспечить конфиденциальность веб-трафика пользователей, не позволяя сетевым посредникам видеть имена хостов, к которым подключается приложение.
Для разработчиков приложений
Чтобы внедрить ECH в ваше приложение:
- Проверьте свою библиотеку для работы с сетью на наличие поддержки ECH : убедитесь, что вы используете версию библиотеки, поддерживающую ECH на Android. Поддержка ECH скоро появится в OkHttp и HttpEngine.
- Настройка параметров сетевой безопасности : по умолчанию ECH включен для всех доменов, если ваша библиотека его поддерживает. Если вам необходимо отключить или принудительно включить ECH, настройте элемент
domainEncryptionв параметрах сетевой безопасности . - Обновите целевой уровень SDK : ECH доступен только на Android 17 (уровень API 37) и выше.
Для разработчиков библиотек
Если вы разрабатываете собственную библиотеку для работы с HTTP-сетями или расширяете существующую, вам следует реализовать поддержку ECH, взаимодействуя с API платформы.
Проверьте политику шифрования домена.
Перед запросом конфигураций ECH или установлением соединений проверьте политику шифрования домена приложения, вызвав метод NetworkSecurityPolicy.getDomainEncryptionMode .
В зависимости от возвращаемого режима, обработка ECH выполняется следующим образом:
-
DOMAIN_ENCRYPTION_MODE_DISABLEDиDOMAIN_ENCRYPTION_MODE_UNKNOWN: Не запрашивать конфигурации ECH и не пытаться выполнить ECH. -
DOMAIN_ENCRYPTION_MODE_ENABLEDиDOMAIN_ENCRYPTION_MODE_OPPORTUNISTIC: Принудительное использование ECH. Получение конфигураций ECH и использование ECH, если сервер его поддерживает. Если сервер не поддерживает ECH, включить ECH GREASE.
Получить конфигурации ECH
Для подключения к ECH необходимо разрешить DNS-запись HTTPS сервера, содержащую конфигурации ECH. Если приложения используют системный DNS, эти данные можно получить одним из двух способов:
Метод 1: Использование высокоуровневого API DnsResolver.query
Если вашей библиотеке не требуются собственные механизмы разрешения DNS, вы можете использовать высокоуровневый API DnsResolver.query платформы. Этот API выполняет параллельные запросы к записям A/AAAA/HTTPS и объединяет результаты в HttpsEndpoint .
Котлин
val resolver = DnsResolver(context, looper)
resolver.query(network, hostname, DnsResolver.TYPE_HTTPS, executor,
DnsResolver.HTTPS_QUERY_WAIT_AUTO, cancellationSignal,
object : DnsResolver.Callback<HttpsEndpoint> {
override fun onAnswer(answer: HttpsEndpoint, rcode: Int) {
val record = answer.httpsRecords.firstOrNull() ?: return
val echConfigList = record.echConfigList ?: return
establishEchConnection(echConfigList)
}
override fun onError(error: DnsResolver.DnsException) { /* Handle error */ }
})
Java
DnsResolver resolver = new DnsResolver(context, looper);
resolver.query(network, hostname, DnsResolver.TYPE_HTTPS, executor,
DnsResolver.HTTPS_QUERY_WAIT_AUTO, cancellationSignal,
new DnsResolver.Callback<HttpsEndpoint>() {
@Override
public void onAnswer(HttpsEndpoint answer, int rcode) {
HttpsRecord record = answer.getHttpsRecords().stream().findFirst().orElse(null);
if (record == null) return;
EchConfigList echConfigList = record.getEchConfigList();
if (echConfigList == null) return;
establishEchConnection(echConfigList);
}
@Override
public void onError(DnsResolver.DnsException error) { /* Handle error */ }
});
Метод 2: Использование getAllByName и DnsResolver.rawQuery
Для библиотек, которые управляют собственными сокетными соединениями и конвейерами разрешения DNS, может быть предпочтительнее разрешать IP-адреса с помощью стандартных API, получая при этом HTTPS-запись отдельно:
- Для разрешения записей A/AAAA используйте
InetAddress.getAllByNameдля сети по умолчанию илиNetwork.getAllByName. - Получите необработанную HTTPS-запись параллельно, используя
DnsResolver.rawQuery. УкажитеDnsResolver.TYPE_HTTPSв качестве типа запроса.
Ответственность разработчика и нестандартные ситуации
Если вы выберете Метод 2, у вашей библиотеки появятся дополнительные обязанности и нестандартные ситуации, которые необходимо учитывать.
- Анализ DNS-записей : Для извлечения
EchConfigListнеобходимо проанализировать необработанные байтовые данные ответа DNS отrawQuery. - Обработка несоответствий в записях : Необходимо обрабатывать несоответствия между запросами A/AAAA и HTTPS.
- Состояние гонки : Необходимо синхронизировать результаты параллельных DNS-запросов. Если один запрос разрешается раньше другого или если запрос HTTPS истекает по таймауту, необходимо соответствующим образом переключиться на резервный вариант (например, попытаться установить стандартное TLS-соединение без ECH, если запрос HTTPS не удается, или использовать ECH GREASE, если он включен политикой).
Настройка TLS
После того как библиотека получит список конфигураций ECH ( EchConfigList ) из HttpsRecord , передайте этот список, используя API утилит SSLSockets или SSLEngines перед началом рукопожатия TLS.
Котлин
fun establishEchConnection(echConfigList: EchConfigList) {
val socket = sslSocketFactory.createSocket(ipAddress, port) as SSLSocket
SSLSockets.setEchConfigList(socket, echConfigList)
socket.startHandshake()
}
Java
public void establishEchConnection(EchConfigList echConfigList)
throws IOException {
SSLSocket socket =
(SSLSocket) sslSocketFactory.createSocket(ipAddress, port);
SSLSockets.setEchConfigList(socket, echConfigList);
socket.startHandshake();
}
Обработка повторных попыток
Если конфигурации ECH на сервере рассинхронизированы, рукопожатие завершается с ошибкой EchConfigMismatchException (подкласс javax.net.ssl.SSLException ). Сервер может включить обновленные конфигурации ECH в свое сообщение об отказе, которые следует использовать для установления нового соединения. Если повторная попытка не предпринимается, несмотря на то, что сервер предоставляет действительные конфигурации для повторной попытки, библиотека должна сообщить об ошибке вызывающему приложению.
Для обработки повторных попыток ECH перехватите исключение и выполните следующие действия:
- Вызовите метод
EchConfigMismatchException.getPublicHostnameпри возникновении исключения. - Проверьте возвращаемое публичное имя хоста с помощью
HostnameVerifier. Если оноnull, прервите соединение. - Если проверка имени хоста прошла успешно, проверьте наличие обновленных конфигураций с помощью
EchConfigMismatchException.getRetryConfigList. - Если доступны обновленные конфигурации, повторите попытку подключения с новым
EchConfigList.
Котлин
try {
socket.startHandshake()
} catch (e: EchConfigMismatchException) {
val publicName = e.publicHostname ?: throw e
if (hostnameVerifier.verify(publicName, socket.session)) {
val retryConfigList = e.retryConfigList
if (retryConfigList != null) {
retryConnection(retryConfigList)
}
} else {
throw e // Hostname mismatch
}
}
Java
try {
socket.startHandshake();
} catch (EchConfigMismatchException e) {
String publicName = e.getPublicHostname();
if (publicName == null) {
throw e;
}
if (hostnameVerifier.verify(publicName, socket.getSession())) {
EchConfigList retryConfigList = e.getRetryConfigList();
if (retryConfigList != null) {
retryConnection(retryConfigList);
}
} else {
throw e; // Hostname mismatch
}
}
Более подробную информацию о процессе повторных попыток см. в RFC 9849 , в частности , почему необходима аутентификация для публичного имени .