O Encrypted Client Hello (ECH) é uma extensão do TLS que criptografa o campo de indicação de nome do servidor (SNI) na mensagem de handshake do cliente. No Android 17 (nível 37 da API) e versões mais recentes, a ECH é compatível por padrão. O ECH ajuda a manter a privacidade do tráfego da Web dos usuários, 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:
- Verifique se sua biblioteca de rede tem suporte para ECH: use uma versão da biblioteca que tenha suporte para ECH no Android:
- OkHttp: a partir da versão 5.5.0, é possível ativar o suporte a ECH
configurando
AndroidDnsouDnsOverHttpsemOkHttpClient.Builder. Para mais informações, consulte o registro de mudanças. - HttpEngine: o suporte será lançado no QPR2 do Android 17 (nível 37.2 da API). Nenhuma configuração especial será necessária.
- WebView: o suporte será adicionado em uma versão futura.
- OkHttp: a partir da versão 5.5.0, é possível ativar o suporte a ECH
configurando
- Configurar a configuração de segurança de rede: por padrão, o ECH é ativado para todos os domínios se a biblioteca for compatível. Se você precisar desativar ou aplicar o ECH,
configure o elemento
domainEncryptionna sua configuração de segurança de rede. - Atualize o nível do SDK de destino: o ECH está disponível apenas no Android 17 (nível 37 da API) e versões mais recentes.
Para desenvolvedores de bibliotecas
Se você estiver desenvolvendo uma biblioteca de rede HTTP personalizada ou estendendo uma existente, implemente o suporte ao ECH interagindo com as APIs da plataforma.
Verificar a política de criptografia de domínio
Antes de consultar configurações de 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_DISABLEDeDOMAIN_ENCRYPTION_MODE_UNKNOWN: não busque configurações de ECH nem tente usar o ECH.DOMAIN_ENCRYPTION_MODE_ENABLEDeDOMAIN_ENCRYPTION_MODE_OPPORTUNISTIC: aplicam o ECH. Recupera configurações de ECH e usa o ECH se o servidor for compatível. Se o servidor não for compatível com o ECH, ative o ECH GREASE.
Recuperar configurações de ECH
Para se conectar com o ECH, resolva 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 de dois métodos:
Método 1: usar a API DnsResolver.query de alto nível
Se a biblioteca não exigir mecanismos personalizados de resolução de DNS, use 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 e buscar o registro HTTPS separadamente:
- Resolva registros A/AAAA usando
InetAddress.getAllByNamepara a rede padrão ouNetwork.getAllByName. - Recupere o registro HTTPS bruto em paralelo usando
DnsResolver.rawQuery. EspecifiqueDnsResolver.TYPE_HTTPScomo o tipo de consulta.
Responsabilidade do desenvolvedor e casos extremos
Se você escolher o método 2, sua biblioteca terá mais responsabilidades e casos extremos a serem considerados.
- Análise de registros DNS: é necessário analisar o payload de bytes brutos da resposta de DNS de
rawQuerypara extrair oEchConfigList. - Como lidar com incompatibilidades de registros: é preciso processar inconsistências entre as consultas A/AAAA e HTTPS.
- Condições de disputa: é necessário sincronizar os resultados das pesquisas de DNS paralelas. Se uma consulta for resolvida antes da outra ou se a consulta HTTPS atingir o tempo limite, faça um fallback adequado. Por exemplo, tente uma conexão TLS padrão sem ECH se a consulta HTTPS falhar ou use ECH GREASE se ele estiver ativado por política.
Configurar a TLS
Depois que a biblioteca recuperar a lista de configurações do ECH
(EchConfigList) do HttpsRecord, transmita essa lista usando as APIs utilitárias
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 novas tentativas
Se as configurações de ECH do servidor ficarem dessincronizadas, o handshake vai falhar
com um EchConfigMismatchException (uma subclasse de
javax.net.ssl.SSLException). O servidor pode incluir configurações de ECH atualizadas
na rejeição, que devem ser usadas para estabelecer uma nova
conexão. Se uma nova tentativa não for feita, mesmo que o servidor forneça configurações válidas, a biblioteca precisará informar um erro ao aplicativo de chamada.
Para processar novas tentativas de ECH, capture a exceção e siga estas etapas:
- Chame
EchConfigMismatchException.getPublicHostnamena exceção. - Verifique o nome do host público retornado usando seu
HostnameVerifier. Se fornull, aborte a conexão. - Se a verificação do nome do host for bem-sucedida, confira as configurações atualizadas usando
EchConfigMismatchException.getRetryConfigList. - Se houver configurações atualizadas, tente se conectar novamente com o novo
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 novas tentativas na RFC 9849, em especial por que é necessário autenticar o nome público.