Encrypted Client Hello (ECH) es una extensión de TLS que encripta el campo Server Name Indication (SNI) en el mensaje de protocolo de enlace del cliente. En Android 17 (nivel de API 37) y versiones posteriores, ECH se admite de forma predeterminada. ECH ayuda a mantener la privacidad del tráfico web de los usuarios, ya que evita que los intermediarios de la red vean los nombres de host a los que se conecta una app.
Para desarrolladores de apps
Para adoptar ECH en tu aplicación, haz lo siguiente:
- Verifica si tu biblioteca de redes admite ECH: Asegúrate de usar una versión de la biblioteca que admita ECH en Android. La compatibilidad estará disponible próximamente en OkHttp y HttpEngine.
- Configura la configuración de seguridad de red: De forma predeterminada, ECH está habilitado para todos los
dominios si tu biblioteca lo admite. Si necesitas inhabilitar o aplicar ECH,
configura el elemento
domainEncryptionen la configuración de seguridad de red. - Actualiza el nivel de SDK objetivo: ECH solo está disponible en Android 17 (nivel de API 37) y versiones posteriores.
Para desarrolladores de bibliotecas
Si desarrollas una biblioteca de redes HTTP personalizada o extiendes una existente, debes implementar la compatibilidad con ECH interactuando con las APIs de la plataforma.
Verifica la política de encriptación de dominio
Antes de consultar las configuraciones de ECH o iniciar conexiones, verifica la política de
encriptación de dominio de la app llamando a
NetworkSecurityPolicy.getDomainEncryptionMode.
Según el modo que se muestre, controla ECH de la siguiente manera:
DOMAIN_ENCRYPTION_MODE_DISABLEDyDOMAIN_ENCRYPTION_MODE_UNKNOWN: No recuperes las configuraciones de ECH ni intentes usar ECH.DOMAIN_ENCRYPTION_MODE_ENABLEDyDOMAIN_ENCRYPTION_MODE_OPPORTUNISTIC: Aplica ECH. Recupera las configuraciones de ECH y usa ECH si el servidor lo admite. Si el servidor no admite ECH, habilita ECH GREASE.
Recupera las configuraciones de ECH
Para conectarte con ECH, debes resolver el registro DNS HTTPS del servidor que contiene las configuraciones de ECH. Cuando las apps usan el DNS del sistema, estos datos se pueden recuperar con uno de estos dos métodos:
Método 1: Usar la API de alto nivel DnsResolver.query
Si tu biblioteca no requiere mecanismos de resolución de DNS personalizados, puedes usar
la API de alto nivel DnsResolver.query de la plataforma. Esta API realiza consultas paralelas
para los registros A/AAAA/HTTPS y combina los resultados en un
HttpsEndpoint.
Kotlin
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 */ }
});
Método 2: Usar getAllByName y DnsResolver.rawQuery
En el caso de las bibliotecas que administran sus propias conexiones de sockets y canalizaciones de resolución de DNS, es posible que prefieras resolver las direcciones IP con APIs estándar mientras recuperas el registro HTTPS por separado:
- Resuelve los registros A/AAAA con
InetAddress.getAllByNamepara la red predeterminada oNetwork.getAllByName. - Recupera el registro HTTPS sin procesar en paralelo con
DnsResolver.rawQuery. EspecificaDnsResolver.TYPE_HTTPScomo el tipo de consulta.
Responsabilidad del desarrollador y casos extremos
Si eliges el método 2, tu biblioteca tiene responsabilidades y casos extremos adicionales que debes tener en cuenta.
- Análisis de registros DNS: Debes analizar la carga útil de bytes sin procesar de la respuesta DNS
de
rawQuerypara extraer elEchConfigList. - Control de desajustes de registros: Debes controlar las incoherencias entre las consultas A/AAAA y HTTPS.
- Condiciones de carrera: Debes sincronizar los resultados de las búsquedas de DNS paralelas. Si una consulta se resuelve antes que la otra o si se agota el tiempo de espera de la consulta HTTPS, debes recurrir a la opción adecuada (por ejemplo, intentar una conexión TLS estándar sin ECH si falla la consulta HTTPS o usar ECH GREASE si la política lo habilita).
Configura TLS
Una vez que la biblioteca haya recuperado la lista de configuración de ECH
(EchConfigList) del HttpsRecord, pasa esta lista con las APIs de utilidad SSLSockets o SSLEngines antes de iniciar el protocolo de enlace TLS.
Kotlin
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();
}
Controla el flujo de reintento
Si las configuraciones de ECH del servidor se desincronizaron, el protocolo de enlace falla con una EchConfigMismatchException (una subclase de javax.net.ssl.SSLException). El servidor puede incluir configuraciones de ECH actualizadas en su rechazo, que se deben usar para establecer una conexión nueva. Si no se intenta un reintento a pesar de que el servidor proporciona configuraciones de reintento válidas, la biblioteca debe informar un error a la aplicación que realiza la llamada.
Para controlar los reintentos de ECH, detecta la excepción y sigue estos pasos:
- Llama a
EchConfigMismatchException.getPublicHostnameen la excepción. - Verifica el nombre de host público que se muestra con tu
HostnameVerifier. Si esnull, anula la conexión. - Si la verificación del nombre de host se realiza correctamente, busca configuraciones actualizadas
con
EchConfigMismatchException.getRetryConfigList. - Si hay configuraciones actualizadas disponibles, vuelve a intentar la conexión con el
nuevo
EchConfigList.
Kotlin
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
}
}
Consulta más detalles sobre el flujo de reintento en RFC 9849, en particular por qué es necesario autenticarse para el nombre público.