Adopta Encrypted Client Hello (ECH)

Encrypted Client Hello (ECH) es una extensión de TLS que encripta el campo de indicación del nombre del servidor (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:

  1. Verifica si tu biblioteca de redes admite ECH: Asegúrate de usar una versión de la biblioteca que admita ECH en Android:
    • OkHttp: A partir de OkHttp 5.5.0, puedes habilitar la compatibilidad con ECH configurando AndroidDns o DnsOverHttps en OkHttpClient.Builder. Para obtener más información, consulta el registro de cambios.
    • HttpEngine: La compatibilidad estará disponible en Android 17 QPR2 (nivel de API 37.2). No se requerirá ninguna configuración especial.
    • WebView: La compatibilidad se agregará en una versión futura.
  2. Configura la configuración de seguridad de la 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 domainEncryption en tu configuración de seguridad de la red.
  3. Actualiza el nivel del SDK de destino: 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.

Cómo verificar la política de encriptación del dominio

Antes de consultar las configuraciones de ECH o iniciar conexiones, llama a NetworkSecurityPolicy.getDomainEncryptionMode para verificar la política de encriptación de dominio de la app.

Según el modo que se devuelva, controla el ECH de la siguiente manera:

  • DOMAIN_ENCRYPTION_MODE_DISABLED y DOMAIN_ENCRYPTION_MODE_UNKNOWN: No se recuperan las configuraciones de ECH ni se intenta usar ECH.
  • DOMAIN_ENCRYPTION_MODE_ENABLED y DOMAIN_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 la configuración de ECH

Para conectarte con ECH, debes resolver el registro DNS HTTPS del servidor que contiene la configuración de ECH. Cuando las apps usan el DNS del sistema, estos datos se pueden recuperar con uno de los siguientes métodos:

Método 1: Usa la API de DnsResolver.query de alto nivel

Si tu biblioteca no requiere mecanismos de resolución de DNS personalizados, puedes usar la API de DnsResolver.query de alto nivel 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: Usa 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:

  1. Resuelve los registros A/AAAA con InetAddress.getAllByName para la red predeterminada o Network.getAllByName.
  2. Recupera el registro HTTPS sin procesar en paralelo con DnsResolver.rawQuery. Especifica DnsResolver.TYPE_HTTPS como el tipo de búsqueda.
Responsabilidad del desarrollador y casos extremos

Si eliges el método 2, tu biblioteca tendrá responsabilidades adicionales y casos extremos que considerar.

  • Análisis de registros DNS: Debes analizar la carga útil de bytes sin procesar de la respuesta DNS de rawQuery para extraer el EchConfigList.
  • Cómo controlar las discrepancias 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 alternativa 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 handshake de 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 reintentos

Si las configuraciones de ECH del servidor se desincronizaron, la negociación falla con un EchConfigMismatchException (una subclase de javax.net.ssl.SSLException). Es posible que el servidor incluya configuraciones de ECH actualizadas en su rechazo, que se deben usar para establecer una nueva conexión. 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 realiza estos pasos:

  1. Llama a EchConfigMismatchException.getPublicHostname en la excepción.
  2. Verifica el nombre de host público devuelto con tu HostnameVerifier. Si es null, anula la conexión.
  3. Si la verificación del nombre de host se realiza correctamente, busca configuraciones actualizadas con EchConfigMismatchException.getRetryConfigList.
  4. Si hay parámetros de configuración actualizados 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 reintentos en el RFC 9849, en particular por qué es necesario autenticarse para el nombre público.