Adopta Encrypted Client Hello (ECH)

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:

  1. 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.
  2. 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 domainEncryption en la configuración de seguridad de red.
  3. 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_DISABLED y DOMAIN_ENCRYPTION_MODE_UNKNOWN: No recuperes las configuraciones de ECH ni intentes 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 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:

  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 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 rawQuery para extraer el EchConfigList.
  • 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:

  1. Llama a EchConfigMismatchException.getPublicHostname en la excepción.
  2. Verifica el nombre de host público que se muestra 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 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.