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 se la tua libreria di rete supporta ECH: assicurati di utilizzare una versione della libreria che supporti ECH su Android. Il supporto sarà disponibile a breve in OkHttp e HttpEngine.
- 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
domainEncryptionin Network Security Config. - Aggiorna il livello SDK target: 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 estendendone una esistente, devi implementare il supporto ECH interagendo con le API della piattaforma.
Controlla la policy di criptaggio del dominio
Prima di eseguire query sulle configurazioni ECH o avviare connessioni, controlla la policy di criptaggio 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 né tentare di utilizzare ECH.DOMAIN_ENCRYPTION_MODE_ENABLEDeDOMAIN_ENCRYPTION_MODE_OPPORTUNISTIC: applica ECH. Recupera le configurazioni ECH e utilizza ECH se il server lo supporta. Se il server non supporta ECH, attiva ECH GREASE.
Recupera le configurazioni ECH
Per connetterti con 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: utilizzo dell'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: utilizzo di 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 separato del record HTTPS:
- 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 libreria ha ulteriori responsabilità e casi limite da considerare.
- Analisi dei record DNS: devi analizzare il payload di byte non elaborato 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 gara: devi sincronizzare i risultati delle ricerche DNS parallele. Se una query viene risolta prima dell'altra o se la query HTTPS va in timeout, 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 dalla policy).
Configura TLS
Una volta che la libreria ha recuperato l'elenco di configurazioni ECH
(EchConfigList) da HttpsRecord, trasmetti questo elenco utilizzando le API di utilità SSLSockets o SSLEngines prima di avviare 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();
}
Gestisci il flusso di nuovi tentativi
Se le configurazioni ECH del server non sono più sincronizzate, l'handshake non va a buon fine con un'eccezione EchConfigMismatchException (una sottoclasse di javax.net.ssl.SSLException). Il server può includere configurazioni ECH aggiornate nel rifiuto, che devono essere utilizzate per stabilire una nuova connessione. Se non viene tentato 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 ECH, intercetta l'eccezione ed esegui questi passaggi:
- Chiama
EchConfigMismatchException.getPublicHostnamesull' eccezione. - Verifica il nome host pubblico restituito utilizzando
HostnameVerifier. Se ènull, interrompi la connessione. - Se la verifica del nome host va a buon fine, controlla se sono presenti configurazioni aggiornate
utilizzando
EchConfigMismatchException.getRetryConfigList. - Se sono disponibili configurazioni aggiornate, riprova a stabilire 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 RFC 9849, in particolare il motivo per cui è necessario eseguire l'autenticazione per il nome pubblico.