Adotar o Encrypted Client Hello (ECH)

O Encrypted Client Hello (ECH) é uma extensão do TLS que criptografa o campo Server Name Indication (SNI) na mensagem de handshake do cliente. No Android 17 (nível 37 da API) e mais recente, o ECH é compatível por padrão. Ele ajuda a manter o tráfego da Web dos usuários particular, impedindo que intermediários de rede vejam os nomes de host a que um app se conecta.

Para desenvolvedores de apps

Para adotar o ECH no seu aplicativo:

  1. Verifique se a biblioteca de rede tem suporte ao ECH: use uma versão da biblioteca que ofereça suporte ao ECH no Android. O suporte será lançado em breve no OkHttp e no HttpEngine.
  2. Configure a configuração de segurança de rede: por padrão, o ECH está ativado para todos os domínios se a biblioteca oferecer suporte a ele. Se você precisar desativar ou aplicar o ECH, configure o elemento domainEncryption na sua configuração de segurança de rede.
  3. Atualize o nível do SDK de destino: o ECH está disponível apenas no Android 17 (nível 37 da API ) e mais recente.

Para desenvolvedores de bibliotecas

Se você estiver desenvolvendo uma biblioteca de rede HTTP personalizada ou estendendo uma já existente, implemente o suporte ao ECH interagindo com as APIs da plataforma.

Verificar a política de criptografia de domínio

Antes de consultar as configurações do ECH ou iniciar conexões, verifique a política de criptografia de domínio do app chamando NetworkSecurityPolicy.getDomainEncryptionMode.

Dependendo do modo retornado, processe o ECH da seguinte maneira:

  • DOMAIN_ENCRYPTION_MODE_DISABLED e DOMAIN_ENCRYPTION_MODE_UNKNOWN: não busque configurações do ECH nem tente usar o ECH.
  • DOMAIN_ENCRYPTION_MODE_ENABLED e DOMAIN_ENCRYPTION_MODE_OPPORTUNISTIC: aplique o ECH. Recupere as configurações do ECH e use-o se o servidor oferecer suporte a ele. Se o servidor não oferecer suporte ao ECH, ative o ECH GREASE.

Recuperar configurações do ECH

Para se conectar com o ECH, você precisa resolver o registro DNS HTTPS do servidor que contém as configurações do ECH. Quando os apps usam o DNS do sistema, esses dados podem ser recuperados usando um dos dois métodos:

Método 1: usar a API DnsResolver.query de alto nível

Se a biblioteca não exigir mecanismos de resolução de DNS personalizados, você poderá usar a API DnsResolver.query de alto nível da plataforma. Essa API faz consultas paralelas para os registros A/AAAA/HTTPS e combina os resultados em um 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 e DnsResolver.rawQuery

Para bibliotecas que gerenciam as próprias conexões de soquete e pipelines de resolução de DNS, talvez seja melhor resolver endereços IP usando APIs padrão ao buscar o registro HTTPS separadamente:

  1. Resolva registros A/AAAA usando InetAddress.getAllByName para a rede padrão ou Network.getAllByName.
  2. Recupere o registro HTTPS bruto em paralelo usando DnsResolver.rawQuery. Especifique DnsResolver.TYPE_HTTPS como o tipo de consulta.
Responsabilidade do desenvolvedor e casos extremos

Se você escolher o Método 2, sua biblioteca terá outras responsabilidades e casos extremos a considerar.

  • Análise de registro DNS: você precisa analisar o payload de bytes brutos da resposta DNS de rawQuery para extrair o EchConfigList.
  • Como processar incompatibilidades de registro: você precisa processar inconsistências entre as consultas A/AAAA e HTTPS.
  • Condições de corrida: você precisa sincronizar os resultados das pesquisas de DNS paralelas. Se uma consulta for resolvida antes da outra ou se a consulta HTTPS expirar, você precisará fazer o fallback de maneira adequada. Por exemplo, tentando uma conexão TLS padrão sem ECH se a consulta HTTPS falhar ou usando ECH GREASE se ele estiver ativado pela política.

Configurar a TLS

Depois que a biblioteca tiver recuperado a lista de configurações do ECH (EchConfigList) do HttpsRecord, transmita essa lista usando as APIs de utilitário SSLSockets ou SSLEngines antes de iniciar o handshake 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();
}

Processar o fluxo de repetição

Se as configurações do ECH do servidor ficarem dessincronizadas, o handshake vai falhar com uma EchConfigMismatchException (uma subclasse de javax.net.ssl.SSLException). O servidor pode incluir configurações atualizadas do ECH na rejeição, que devem ser usadas para estabelecer uma nova conexão. Se uma repetição não for tentada, mesmo que o servidor forneça configurações de repetição válidas, a biblioteca precisará informar um erro ao aplicativo de chamada.

Para processar repetições do ECH, capture a exceção e siga estas etapas:

  1. Chame EchConfigMismatchException.getPublicHostname na exceção.
  2. Verifique o nome do host público retornado usando seu HostnameVerifier. Se for null, aborte a conexão.
  3. Se a verificação do nome do host for bem-sucedida, verifique se há configurações atualizadas usando EchConfigMismatchException.getRetryConfigList.
  4. Se as configurações atualizadas estiverem disponíveis, tente novamente a conexão com a nova 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
    }
}

Confira mais detalhes sobre o fluxo de repetição na RFC 9849, em particular por que é necessário autenticar o nome público.