Encrypted Client Hello (ECH) è un'estensione TLS che cripta il campo Server Name Indication (SNI) nel messaggio di handshake del client. In Android 17 (livello API 37) e versioni successive, ECH è supportato per impostazione predefinita. ECH contribuisce a mantenere privato il traffico web degli utenti impedendo agli intermediari di rete di visualizzare i nomi host a cui si connette un'app.
Per gli sviluppatori di app
Per adottare ECH nella tua applicazione:
- Controlla la tua libreria di rete per il supporto di ECH: assicurati di utilizzare una versione della libreria che supporti ECH su Android:
- OkHttp: a partire da OkHttp 5.5.0, puoi attivare il supporto di ECH configurando
AndroidDnsoDnsOverHttpssuOkHttpClient.Builder. Per ulteriori informazioni, consulta il log delle modifiche. - HttpEngine: il supporto sarà disponibile in Android 17 QPR2 (livello API 37.2). Non sarà necessaria alcuna configurazione speciale.
- WebView: il supporto verrà aggiunto in una release futura.
- OkHttp: a partire da OkHttp 5.5.0, puoi attivare il supporto di ECH configurando
- Configura Network Security Config: per impostazione predefinita, ECH è abilitato per tutti i domini se la tua libreria lo supporta. Se devi disattivare o applicare ECH,
configura l'elemento
domainEncryptionnella configurazione della sicurezza di rete. - Aggiorna il livello SDK di destinazione: ECH è disponibile solo su Android 17 (livello API 37) e versioni successive.
Per gli sviluppatori di librerie
Se stai sviluppando una libreria di rete HTTP personalizzata o estendendo una libreria esistente, devi implementare il supporto ECH interagendo con le API della piattaforma.
Controllare le norme di crittografia del dominio
Prima di eseguire query sulle configurazioni ECH o avviare connessioni, controlla le
norme di crittografia del dominio dell'app chiamando
NetworkSecurityPolicy.getDomainEncryptionMode.
A seconda della modalità restituita, gestisci ECH nel seguente modo:
DOMAIN_ENCRYPTION_MODE_DISABLEDeDOMAIN_ENCRYPTION_MODE_UNKNOWN: non recuperare le configurazioni ECH o tentare di utilizzare ECH.DOMAIN_ENCRYPTION_MODE_ENABLEDeDOMAIN_ENCRYPTION_MODE_OPPORTUNISTIC: applica ECH. Recuperare le configurazioni ECH e utilizzare ECH se il server lo supporta. Se il server non supporta ECH, attiva ECH GREASE.
Recuperare le configurazioni ECH
Per connetterti a ECH, devi risolvere il record DNS HTTPS del server contenente le configurazioni ECH. Quando le app utilizzano il DNS di sistema, questi dati possono essere recuperati utilizzando uno dei due metodi seguenti:
Metodo 1: utilizza l'API DnsResolver.query di alto livello
Se la tua libreria non richiede meccanismi di risoluzione DNS personalizzati, puoi utilizzare
l'API DnsResolver.query di alto livello della piattaforma. Questa API esegue query
parallele per i record A/AAAA/HTTPS e combina i risultati in 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 */ }
});
Metodo 2: utilizzare getAllByName e DnsResolver.rawQuery
Per le librerie che gestiscono le proprie connessioni socket e pipeline di risoluzione DNS, potresti preferire risolvere gli indirizzi IP utilizzando le API standard durante il recupero del record HTTPS separatamente:
- Risolvi i record A/AAAA utilizzando
InetAddress.getAllByNameper la rete predefinita oNetwork.getAllByName. - Recupera il record HTTPS non elaborato in parallelo utilizzando
DnsResolver.rawQuery. SpecificaDnsResolver.TYPE_HTTPScome tipo di query.
Responsabilità dello sviluppatore e casi limite
Se scegli il metodo 2, la tua biblioteca ha ulteriori responsabilità e casi limite da considerare.
- Analisi del record DNS: devi analizzare il payload di byte non elaborati della risposta DNS da
rawQueryper estrarreEchConfigList. - Gestione delle mancate corrispondenze dei record: devi gestire le incoerenze tra le query A/AAAA e HTTPS.
- Condizioni di competizione: devi sincronizzare i risultati delle ricerche DNS parallele. Se una query viene risolta prima dell'altra o se la query HTTPS scade, devi eseguire il fallback in modo appropriato (ad esempio, tentando una connessione TLS standard senza ECH se la query HTTPS non va a buon fine o utilizzando ECH GREASE se è abilitato tramite policy).
Configurare TLS
Una volta che la libreria ha recuperato l'elenco delle configurazioni ECH
(EchConfigList) da HttpsRecord, passa questo elenco utilizzando le API di utilità
SSLSockets o SSLEngines prima di iniziare l'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();
}
Gestire il flusso di nuovi tentativi
Se le configurazioni ECH del server non sono più sincronizzate, l'handshake non riesce
con un errore EchConfigMismatchException (una sottoclasse di
javax.net.ssl.SSLException). Il server potrebbe includere configurazioni ECH aggiornate nel rifiuto, che devono essere utilizzate per stabilire una nuova connessione. Se non viene eseguito un nuovo tentativo nonostante il server fornisca configurazioni di nuovi tentativi valide, la libreria deve segnalare un errore all'applicazione chiamante.
Per gestire i nuovi tentativi di ECH, intercetta l'eccezione ed esegui questi passaggi:
- Chiama
EchConfigMismatchException.getPublicHostnamein merito all'eccezione. - Verifica il nome host pubblico restituito utilizzando il tuo
HostnameVerifier. Se ènull, interrompi la connessione. - Se la verifica del nome host ha esito positivo, controlla le configurazioni aggiornate
utilizzando
EchConfigMismatchException.getRetryConfigList. - Se sono disponibili configurazioni aggiornate, riprova la connessione con il
nuovo
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
}
}
Per ulteriori dettagli sul flusso di nuovi tentativi, consulta l'RFC 9849, in particolare perché è necessario autenticarsi per il nome pubblico.