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:
- 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.
- 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
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 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_DISABLEDeDOMAIN_ENCRYPTION_MODE_UNKNOWN: não busque configurações do ECH nem tente usar o ECH.DOMAIN_ENCRYPTION_MODE_ENABLEDeDOMAIN_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:
- 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á outras responsabilidades e casos extremos a considerar.
- Análise de registro DNS: você precisa analisar o payload de bytes brutos da resposta DNS
de
rawQuerypara extrair oEchConfigList. - 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:
- 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, verifique se há configurações atualizadas
usando
EchConfigMismatchException.getRetryConfigList. - 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.